diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8837e52..c1744988 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,10 +29,25 @@ jobs: # a couple of modules import pyplot at load time and would otherwise complain. env: MPLBACKEND: Agg + # The engine is DETERMINISTIC BY DESIGN (PYTHONHASHSEED=0, hashlib not hash(), seeded RNG). Multi-threaded BLAS + # breaks that: it sums float dot-products in a different ORDER per run, which flips knife-edge results (a cosine + # that rounds to 1.0000000000000002 instead of 1.0; a borderline z-score). So pin numpy to ONE thread and fix + # the hash seed -- this restores the deterministic reference the tests assume. With `-n auto` it's also FASTER: + # each xdist worker gets its own core with a single BLAS thread, instead of 4 workers x N BLAS threads fighting + # over the cores (oversubscription). If a test only fails in CI and passes locally, this is usually why. + PYTHONHASHSEED: "0" + OMP_NUM_THREADS: "1" + OPENBLAS_NUM_THREADS: "1" + MKL_NUM_THREADS: "1" + NUMEXPR_NUM_THREADS: "1" steps: - name: Check out the code uses: actions/checkout@v4 + with: + # Full history so a pull_request can diff against its base branch's merge-base (origin/base...HEAD) to find + # exactly the files this PR changed. Cheap for this repo; needed by the affected-tests selection below. + fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v5 @@ -46,4 +61,76 @@ jobs: pip install -r requirements.txt - name: Run the test suite - run: pytest -q --durations=10 + # Two speed levers together: + # 1. -n auto spreads tests across ALL the runner's CPU cores (ubuntu-latest has 4). The suite is + # deterministic and each test is independent (own tmp_path / in-memory stores), so it parallelises cleanly. + # 2. On a PULL REQUEST we run only the tests AFFECTED by the changed files -- tools/select_tests.py builds a + # static import graph and picks every test whose imports (transitively) reach a changed module. A test that + # can't reach any changed file doesn't run. On a PUSH TO MAIN we run the WHOLE suite, so main is always + # fully verified -- the affected-only run is fast PR feedback, the full run is the safety net. + # It fails safe: if a change can't be scoped (a data file, a brand-new module) select_tests prints "ALL" and we + # run everything; if nothing is affected (docs only) we skip the run (the doc gates below still check docs). + run: | + set -e + if [ "${{ github.event_name }}" = "pull_request" ]; then + CHANGED=$(git diff --name-only "origin/${{ github.base_ref }}...HEAD") + echo "Changed files in this PR:"; echo "$CHANGED" + SELECTED=$(python tools/select_tests.py $CHANGED) + if [ "$SELECTED" = "ALL" ]; then + echo "== change can't be scoped -> running the FULL suite ==" + pytest -q -n auto --durations=15 + elif [ -z "$SELECTED" ]; then + echo "== no tests affected by these changes (docs/config only) -- skipping the test run ==" + else + echo "== running only the affected tests =="; echo "$SELECTED" + pytest -q -n auto --durations=15 $SELECTED + fi + else + echo "== push to ${{ github.ref_name }} -> running the FULL suite ==" + pytest -q -n auto --durations=15 + fi + + # Keep API_QUICKREF.md current on its own: regenerate it and fail if the committed copy is stale. + # This is a drift check (no write-back needed) -- if it fails, run `python apiquickref.py` locally and + # commit the result. The generator only reads source with `ast`, so this step is fast and side-effect-free. + - name: Check API_QUICKREF.md is up to date + run: | + python apiquickref.py + git diff --exit-code API_QUICKREF.md || { + echo "API_QUICKREF.md is out of date -- run 'python apiquickref.py' and commit the result."; + exit 1; + } + + # DISCOVERABILITY GATE. Every capability family a user would ask for must have a curated catalog home, so the + # engine can surface it from a plain-English query (mind.find_capability / mind.suggest). This fails if any + # probe query in tools/catalog_gaps.py has no curated home -- i.e. something is built but effectively hidden. + # Fix by registering a home in holographic_catalog.py (see the existing ones for the shape). + - name: Gate -- no discoverability gaps + run: python tools/catalog_gaps.py + + # INVOCATION GATE. Every public UnifiedMind faculty must carry a docstring an agent can act on (skill cards are + # built from the first line), and every "how to call it" example in a catalog home must actually resolve -- + # both module imports and mind.method() references. This fails on a missing/thin docstring or a broken example, + # so a capability can never be discoverable-but-uncallable. Fix by writing the one-line docstring / correcting + # the example the linter names. + - name: Gate -- no invocation gaps + run: python tools/skill_lint.py + + # Keep CAPABILITIES.md (the plain-language "what can it do + how to start" menu) current. Same drift-check + # pattern as API_QUICKREF.md: regenerate from the live catalog and fail if the committed copy is stale. + # capdoc.py writes no timestamp, so this only trips on a real change. Fix: `python capdoc.py` and commit. + - name: Check CAPABILITIES.md is up to date + run: | + python capdoc.py + git diff --exit-code CAPABILITIES.md || { + echo "CAPABILITIES.md is out of date -- run 'python capdoc.py' and commit the result."; + exit 1; + } + + # ENDPOINT + FLAG GATE. SERVICE.md is mostly hand-written prose (kept), but its endpoint table must list exactly + # the routes the service registers, and its Launch section must mention every user-facing CLI flag the argparse + # defines. This checks both against the live code and fails if a route or flag was added/renamed/removed without + # updating the doc. It does NOT rewrite the prose; on failure it names the offender, and `python servicedoc.py + # --print` gives a fresh endpoint table to paste. + - name: Gate -- SERVICE.md documents every endpoint and flag + run: python servicedoc.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..10afd1f4 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,42 @@ +# .github/workflows/docs.yml +# Keep REFERENCE.md in sync with the code automatically. On every push to main, regenerate the code reference +# from the module docstrings and commit it back if anything changed. (No third-party deps -- docgen.py is pure +# standard library.) +name: docs + +on: + push: + branches: [ main, master ] + workflow_dispatch: # lets you run it by hand from the Actions tab + +permissions: + contents: write # needed so the job can commit the refreshed REFERENCE.md back to the repo + +jobs: + reference: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Generate the code reference + run: python docgen.py # writes REFERENCE.md from every module's docstring + public API + + - name: Generate the capabilities menu + run: python capdoc.py # writes CAPABILITIES.md from the live capability catalog (plain-language menu) + + - name: Commit refreshed docs if they changed + run: | + git config user.name "docs-bot" + git config user.email "docs-bot@users.noreply.github.com" + if ! git diff --quiet REFERENCE.md CAPABILITIES.md; then + git add REFERENCE.md CAPABILITIES.md + git commit -m "docs: refresh REFERENCE.md + CAPABILITIES.md [skip ci]" # [skip ci] so it doesn't re-trigger CI + git push + else + echo "REFERENCE.md and CAPABILITIES.md already up to date -- nothing to commit." + fi diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 00000000..afcd7ca4 --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,96 @@ +# .github/workflows/package.yml +# Build an installable leOS-core wheel on every push to main and on version tags, smoke-test that it imports, +# upload it as a build artifact, attach it to a GitHub Release on a tag, and -- on a version tag -- publish it +# to PyPI so people can `pip install leos-core` (and then `import lecore`). +name: package + +on: + push: + branches: [ main, master ] + tags: [ 'v*' ] + workflow_dispatch: # lets you run it by hand from the Actions tab + +jobs: + # --------------------------------------------------------------------------------------------------------- + # Job 1: build the wheel + sdist, prove the wheel imports, and stash the built files as an artifact. + # --------------------------------------------------------------------------------------------------------- + build-wheel: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install the build tool + run: python -m pip install --upgrade pip build + + # On a version tag, make sure the tag and setup.py agree BEFORE building/publishing. Catches the easy mistake + # of tagging v0.2.0 while setup.py still says 0.1.0 -- which would otherwise publish the wrong version (and PyPI + # never lets you re-upload a version). No-op on branch pushes (there's no version tag to check). Fix on failure: + # bump setup.py's version= to match the tag, or tag the version setup.py already has. + - name: Check the tag matches setup.py version + if: startsWith(github.ref, 'refs/tags/v') + run: python tools/check_version.py --expect "${{ github.ref_name }}" + + - name: Build the package (copies essentials, strips tests, builds the wheel) + run: sh build_package.sh + + - name: Smoke-test the wheel (install it clean, in isolation, and exercise its bundled data) + run: | + python -m pip install dist/*.whl + # Run from a TEMP dir, not the repo checkout, so nothing resolves against repo files by accident -- + # this proves the WHEEL is self-contained, including the vendored data it needs at runtime. + cd "$(mktemp -d)" + # The distribution is "leos-core" but the import name is still "lecore" (via the lecore.py shim). + python -c "import lecore; assert hasattr(lecore, 'UnifiedMind'); print('leOS-core installed; import lecore OK')" + # The runtime data package must be present AND its dictionary must load -- this is the check that would + # have caught a wheel shipped without its data. If it fails, the data isn't in package_data / build_package.sh. + python -c "import lecore_data; assert lecore_data.exists('knowledge', 'dictionary.json.gz'), 'vendored dictionary missing from the wheel'" + python -c "import holographic_dictionary as d; assert d.size() > 100000; print('dictionary works from the wheel:', d.size(), 'words')" + + - name: Upload the wheel + sdist as build artifacts + uses: actions/upload-artifact@v4 + with: + name: leos-core-dist # the later publish job downloads this exact name + path: dist/* + + - name: Attach to the GitHub Release (only when a version tag is pushed) + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + files: dist/* + + # --------------------------------------------------------------------------------------------------------- + # Job 2: publish to PyPI. Runs ONLY on a version tag (v*), and ONLY after the build job succeeded. + # + # This uses PyPI "Trusted Publishing" (OIDC): GitHub proves to PyPI who it is, so there is NO API token or + # password stored anywhere in this repo. The `id-token: write` permission below is what lets that handshake + # happen. You do a ONE-TIME setup on PyPI first -- see PACKAGING.md for the exact clicks: + # PyPI -> your project (or "pending publisher") -> Publishing -> add a GitHub trusted publisher with + # owner = AnOversizedMooseWithSocks, repo = leCore, workflow = package.yml. + # After that, pushing a tag like v0.1.0 publishes that version automatically. + # --------------------------------------------------------------------------------------------------------- + publish-pypi: + needs: build-wheel # don't publish unless the wheel built and imported cleanly + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + permissions: + id-token: write # REQUIRED for trusted publishing (the OIDC handshake). No token needed. + # environment: pypi # optional: create a GitHub Environment named "pypi" for an extra + # approval gate, then set the same name on PyPI's publisher config + # and uncomment this line. Leave commented for the simplest setup. + steps: + - name: Download the built wheel + sdist from Job 1 + uses: actions/download-artifact@v4 + with: + name: leos-core-dist + path: dist # the publish action expects the files in dist/ + + - name: Publish to PyPI (trusted publishing -- no stored secret) + uses: pypa/gh-action-pypi-publish@release/v1 + # Defaults publish to the real PyPI. To rehearse on TestPyPI first, uncomment: + # with: + # repository-url: https://test.pypi.org/legacy/ diff --git a/.gitignore b/.gitignore index 13d5ab46..45a54d68 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,18 @@ /.venv /__pycache__ +holostuff.zip +/current backlogs +holostuff_chunking_transfer_backlog.md +holostuff_consolidation_backlog.md +holostuff_crosscutting_backlog.md +holostuff_holographic_dcc_backlog.md +holostuff_inverse_rendering_backlog.md +holostuff_panel_generative_compression.md +holostuff_panel_integration_review.md +holostuff_panel_proven_structure.md +holostuff_panel_review_crosscutting.md +holostuff_panel_review_rendering_backlog.md +holostuff_rendering_backlog.md +holostuff_vm_backlog.md +PANEL_occlusion_speed_backlog.md +RENDER_PIPELINE_BACKLOG.md diff --git a/API_QUICKREF.md b/API_QUICKREF.md new file mode 100644 index 00000000..0cf6a183 --- /dev/null +++ b/API_QUICKREF.md @@ -0,0 +1,235 @@ +# leCore API Quick Reference + +*A scannable, one-line-per-symbol map of the app-building surface -- auto-generated by `apiquickref.py` on 2026-07-05. For the full engine (every module), see REFERENCE.md.* + +## Scene authoring + +### `holographic_scene_doc` +*holographic_scene_doc.py -- the canonical Scene document (modeling-app backlog, item 0: A + B + E).* + +- **class `SceneObject`** -- One object in the scene, as a role-bound RECORD: a stable handle plus its properties. +- **class `Scene`** -- The single source of truth: a table of object records + a hierarchy, owning selection and undo history and firing change events. + - `on_change(self, callback)` -- Register callback(kind, handle), fired on every change. + - `handle_vector(self, handle)` -- The object's permanent identity atom -- its holographic handle (used for labelled bundles / query). + - `add(self, name=None, transform=None, geometry=None, material=None, tags=None, params=None, parent=None, overrides=None, _record=True)` -- Add an object; return its STABLE handle. + - `edit(self, handle, _record=True, **changes)` -- Mutate an object's fields (name/transform/geometry/material/tags/params). + - `remove(self, handle, _record=True)` -- Remove an object (and drop it from the selection/hierarchy). + - `remove_tag(self, handle, key, _record=True)` -- Remove one tag from an object. + - `clear_override(self, handle, prop, _record=True)` -- Remove one render override from an object, so it FALLS BACK to the scene default. + - `select(self, handles)` -- Set the current selection (a set of handles) and fire a 'select' event. + - `get(self, handle)` -- + - `set_parent(self, child, parent, _record=True)` -- Parent `child` under `parent` (both handles; parent=None for top level). + - `parent_of(self, handle)` -- The parent handle of an object (None if top level). + - `children_of(self, handle)` -- + - `begin_group(self, label='Edit')` -- Open a transaction: every mutation until end_group() coalesces into ONE undo step labelled `label`. + - `end_group(self)` -- Close the current transaction; commit the accumulated changes as a single step (nothing if empty). + - `group(self, label='Edit')` -- `with scene.group("Move wheels"): ...` -- everything inside becomes one undo step. + - `undo(self)` -- Undo the last STEP by restoring its BEFORE snapshots (in reverse order within the step). + - `redo(self)` -- Redo the last undone step by restoring its AFTER snapshots. + - `history(self)` -- The undo stack's step labels, oldest first. + - `redo_history(self)` -- The redo stack's step labels (most-recently-undone last). + - `can_undo(self)` -- + - `can_redo(self)` -- + +### `holographic_modifier` +*holographic_modifier.py -- the per-object MODIFIER STACK + dependency graph (modeling-app backlog, items C + D).* + +- **class `Modifier`** -- One entry in the stack: a named operation with parameters, applied non-destructively to the previous result. +- **class `ModifierStack`** -- A per-object modifier stack: a base payload + an ordered list of modifiers, evaluated non-destructively, re-evaluated O(change) (only downstream of a change), with stable handles and validation. + - `handles(self)` -- + - `names(self)` -- + - `add(self, name, op, params=None, specs=None, muted=False)` -- Append a modifier to the top of the stack; returns its stable handle. + - `insert(self, index, name, op, params=None, specs=None, muted=False)` -- Insert a modifier at `index` (everything from there down must recompute). + - `remove(self, handle)` -- + - `move(self, handle, to_index)` -- Reorder a modifier (a real modeling operation -- bevel-then-subdivide differs from the reverse). + - `set_muted(self, handle, muted)` -- + - `set_param(self, handle, **params)` -- Change a modifier's parameters -- the common case a dependency graph optimises. + - `evaluate(self)` -- Fold the stack over the base, non-destructively. + - `validate(self)` -- Well-formedness (like recipeops.validate): every op callable, and every declared param within its min/max. + - `describe(self, handle)` -- The property-panel schema for one modifier: each parameter's name, type, current value, and (if the modifier declared specs) default/min/max. +- `describe_object(obj)` -- Item D over a SceneObject: enumerate its editable roles (name, material, tags, params) as a schema for a property panel. + +## Geometry / SDF + +### `holographic_sdf` +*Holographic SDF / shader algebra (S1): a 3D signed-distance expression tree that evaluates, composes, represents itself holographically, and reads/writes both a compact DSL and a Shadertoy-ready GLSL shader.* + +- `sdf_normal(sdf, P, eps=0.001)` -- The surface normal at points P:(M,3) = the normalised gradient of the SDF, by central differences (6 vectorised evals). +- **class `SDF`** -- A node in a signed-distance expression tree: `kind`, scalar `params`, and child SDFs. + - `eval(self, P)` -- + - `union(self, other)` -- + - `intersect(self, other)` -- + - `subtract(self, other)` -- + - `smooth_union(self, other, k=0.3)` -- + - `translate(self, t)` -- + - `scale(self, s)` -- + - `rotate(self, axis, angle)` -- + - `repeat(self, period)` -- + - `rounded(self, r)` -- + - `onion(self, thickness)` -- + - `displace(self, amount, freq)` -- + - `twist(self, k)` -- + - `to_tree(self)` -- A nested tuple where the op name folds in the params (e.g. + - `to_dsl(self)` -- A compact s-expression: (kind p0 p1 ... + - `to_glsl(self, name='map')` -- Emit a complete Shadertoy-ready fragment shader for this SDF (see _emit_shader). +- `sphere(r=1.0)` -- A sphere of radius `r`, centred at the origin. +- `box(bx=1.0, by=1.0, bz=1.0)` -- An axis-aligned box with half-extents (bx, by, bz) centred at the origin -- so the box spans [-bx, bx] on x, etc. +- `torus(R=1.0, r=0.3)` -- A torus in the XZ plane: `R` is the ring radius (centre to tube centre), `r` the tube radius. +- `cylinder(h=1.0, r=0.5)` -- A capped cylinder of half-height `h` and radius `r`, axis along Y, centred at the origin. +- `plane(h=0.0)` -- An infinite ground plane at height y = `h` (points above are outside). +- `menger(iterations=3, size=1.0)` -- The Menger sponge: the classic recursive fractal cube, carved `iterations` deep at the given `size`. +- `to_callable(node)` -- Wrap an SDF tree as a plain `sdf(P)->dist` callable for mesh_from_sdf / marching. +- `parse_dsl(text)` -- Parse a (kind p0 ... +- `node_kinds(node)` -- The set of kinds used anywhere in the tree (for the inexact-warp warning and for tests). + +### `holographic_sdfscene` +*holographic_sdfscene.py -- a small, documented base class for "a scene is a set of SDF parts".* + +- **class `SDFScene`** -- Subclass and implement `parts()` -> list of (sdf_fn, material_name). + - `parts(self)` -- Return a list of (sdf_fn, material_name). + - `eval(self, P)` -- Nearest-surface signed distance at each point = min over parts. + - `part_ids(self, P)` -- Which part owns each point = argmin over parts (for material lookup). + - `material_at(self, P)` -- The material_name of the owning part at each point (or None where the scene is empty). + - `bounds(self)` -- Override to return a list of (center_xyz, radius) bounding spheres, one per part, each enclosing that part's surface. + - `parts_near(self, point, radius)` -- Indices of parts whose bounding sphere lies within `radius` of `point`, using the SpatialGrid. + +### `holographic_mesh` +*The explicit polygon mesh kernel (FWD-1): the substrate every explicit-geometry operator mutates.* + +- **class `Mesh`** -- An explicit polygon mesh: positions + faces, with optional per-vertex attributes. + - `n_vertices(self)` -- + - `n_faces(self)` -- + - `edges(self)` -- The set of UNDIRECTED edges as frozensets {vi, vj}. + - `n_edges(self)` -- + - `half_edges(self)` -- Build (and cache) the half-edge table. + - `vertex_faces(self, v)` -- The faces incident to vertex `v`, as a sorted list of face indices (deterministic). + - `vertex_neighbours(self, v)` -- The 1-ring of vertex `v`: vertices sharing an edge with it, as a sorted list (deterministic). + - `euler_characteristic(self)` -- chi = V - E + F. + - `is_closed(self)` -- True iff the mesh has no boundary: every half-edge has a twin (the surface fully wraps). + - `is_manifold(self)` -- True iff the half-edge structure built without raising (no directed edge appeared twice) AND -- the boundary-aware part -- every undirected edge is shared by at most two faces. + - `genus(self)` -- The genus g of a CLOSED orientable mesh from chi = 2 - 2g. + - `validate_topology(self)` -- The full well-formedness report for a 3-D modeling app: does this mesh have the clean, 2-manifold topology a user expects, or are there elements that will surprise them? Returns a dict: ok -- True iff manifold AND no degenerate faces (the one flag to gate on) manifold_edges -- True iff every undirected edge is shared by <= 2 faces, consistently oriented manifold_vertices -- True iff every vertex's face-fan is a single connected disk/half-disk (no BOWTIE: two cones meeting at one point pass the edge test but are non-manifold) watertight -- True iff closed (every edge has exactly two faces; no boundary/holes) non_manifold_edges -- list of (lo,hi) undirected edges shared by >2 faces (or mis-oriented) non_manifold_verts -- list of vertex indices whose link splits into >1 component (bowties) boundary_edges -- count of edges on a boundary (shared by exactly one face) degenerate_faces -- count of faces with a repeated vertex index (zero-area / collapsed) euler, genus -- the combinatorial invariants (chi = V-E+F; genus for a closed surface) Edge-manifoldness and orientation are decided by the half-edge build (a directed edge appearing twice is non-manifold). + - `vertex_normals(self, store=True)` -- Per-vertex shading normals by Newell's method: each face contributes a normal whose magnitude is proportional to the face area (so big faces weigh more), accumulated at each of the face's vertices, then normalised per vertex. + - `triangulate(self)` -- Fan-triangulate every polygon: a face [v0, v1, ..., v_{n-1}] becomes triangles (v0,v1,v2), (v0,v2,v3), ... + - `to_buffers(self)` -- The flat, INDEXED buffers a glTF / three.js renderer consumes: position : (V, 3) float32 -- always present normal : (V, 3) float32 -- the stored normals, or freshly computed if absent uv : (V, 2) float32 -- only if the mesh has uvs colour : (V, 4) float32 -- only if the mesh has colours indices : (T*3,) int -- the triangle index buffer (flattened) float32 because that is what glTF stores; the integer index buffer is exact. + - `from_buffers(position, indices, normal=None, uv=None, colour=None)` -- Reconstruct a (triangle) Mesh from flat buffers -- the inverse of `to_buffers`. + - `to_obj(self)` -- Serialise to a Wavefront OBJ string: `v x y z` lines then `f i j k ...` lines (1-indexed, OBJ's convention). + - `from_obj(text)` -- Parse a minimal Wavefront OBJ (v / f lines). +- `box(width=1.0, height=1.0, depth=1.0, center=(0.0, 0.0, 0.0))` -- An axis-aligned box as a QUAD mesh: 8 vertices, 6 quad faces, consistently oriented (outward CCW). +- `tetrahedron(scale=1.0, center=(0.0, 0.0, 0.0))` -- A regular tetrahedron as 4 triangles: V=4, E=6, F=4, chi=2. +- `grid(nx=4, ny=4, width=1.0, height=1.0, center=(0.0, 0.0, 0.0))` -- A flat subdivided plane in the z=0 plane: an (nx by ny) grid of quads. + +## Transforms + +### `holographic_transform` +*holographic_transform.py -- TRANSFORM UTILITIES for a modeling app (modeling-app backlog, item G).* + +- `translation(t)` -- A 4x4 translation matrix from a 3-vector. +- `scaling(s)` -- A 4x4 scale matrix. +- `rotation_axis_angle(axis, angle)` -- A 4x4 rotation of `angle` radians about `axis` (Rodrigues' formula). +- `compose(*mats)` -- Matrix product M0 @ M1 @ ... +- `decompose(M)` -- Split a 4x4 affine transform into (translate (3,), rotation quaternion (4,), scale (3,)). +- `compose_trs(translate, quat, scale)` -- Build a 4x4 from translate (3,), a rotation quaternion (4,), and scale (3,) -- the inverse of decompose. +- `quat_normalize(q)` -- +- `quat_mul(a, b)` -- The Hamilton product a*b: the rotation "apply b, then a". +- `quat_from_axis_angle(axis, angle)` -- A quaternion for a rotation of `angle` radians about `axis`. +- `quat_to_axis_angle(q)` -- Recover (axis, angle) from a quaternion. +- `quat_to_matrix(q)` -- The 3x3 rotation matrix for a quaternion. +- `quat_from_matrix(R)` -- The quaternion for a 3x3 rotation matrix (Shepperd's method: branch on the largest diagonal term for numerical stability -- a naive formula loses precision when the trace is near zero). +- `quat_from_euler(rx, ry, rz)` -- A quaternion from euler angles applied X then Y then Z (R = Rz @ Ry @ Rx). +- `quat_to_euler(q)` -- Recover euler angles (rx, ry, rz) from a quaternion, inverting R = Rz @ Ry @ Rx. +- `quat_slerp(a, b, t)` -- Spherical linear interpolation between two rotations -- constant angular speed, the smooth in-between an animation wants. +- `quat_rotate(q, v)` -- Rotate a 3-vector by a quaternion. +- `look_at(eye, target, up=(0.0, 1.0, 0.0))` -- An OpenGL view matrix for a camera at `eye` looking at `target` (the engine's convention: the camera looks down -z, y is up). + +## Camera + +### `holographic_camera` +*holographic_camera.py -- the CAMERA CONTROLLER: viewport navigation (modeling-app feature layer).* + +- **class `CameraController`** -- Orbit / pan / dolly / zoom / frame around a target. + - `distance(self)` -- The eye-to-target distance (the orbit radius). + - `orbit(self, d_azimuth, d_elevation, elevation_limit=...)` -- Rotate the eye around the target: `d_azimuth` about the world up axis, `d_elevation` about the current right axis. + - `pan(self, dx, dy)` -- Slide the camera in its own right/up plane -- both eye and target move by the same vector, so the view direction and distance are unchanged (a translation of the whole rig). + - `dolly(self, distance)` -- Move the eye toward (+) or away from (-) the target along the view direction. + - `zoom(self, factor)` -- Scale the orbit radius by `factor` (01 pulls back). + - `frame(self, bbox_min, bbox_max, fov_deg=45.0)` -- Aim at the box centre and back off just far enough that its bounding SPHERE fills the vertical field of view: distance = radius / sin(fov/2). + - `view_matrix(self)` -- The 4x4 OpenGL view matrix for the current pose (via item-G look_at). + - `to_camera(self, fov_deg=45.0, aspect=1.0)` -- A render Camera at the current pose (for handing straight to the path tracer / session). + +## Rendering + +### `holographic_render` +*A CPU rendering subsystem (RND-1): camera, lights, a mesh rasteriser, and a volumetric ray-marcher.* + +- **class `Camera`** -- A pinhole camera. + - `view_matrix(self)` -- World -> camera (look-at). + - `projection_matrix(self)` -- Perspective projection (OpenGL-style, maps the frustum to the [-1,1] cube). + - `ray_dirs(self, width, height, jitter=None)` -- Per-pixel world-space ray origins (the eye) and unit directions, shape (H, W, 3), for ray marching. +- **class `Light`** -- A light. +- `rasterize_mesh(mesh, camera, width=512, height=512, lights=None, base_color=(0.8, 0.8, 0.8), background=(0.05, 0.06, 0.08), ambient=0.15, vectorized=True)` -- Rasterise a triangle mesh to an (H, W, 3) RGB image in [0,1] with a z-buffer and per-face Lambert shading. +- `volume_render(field, camera, bounds, width=256, height=256, steps=96, mode='smoke', sigma=12.0, emission_color=None, albedo=(0.9, 0.9, 0.95), lights=None, background=(0.0, 0.0, 0.0), early_term=True, empty_skip=True, occ_res=24, occ_thresh=0.001, term_eps=0.002)` -- Render a density FIELD (callable points(N,3)->density>=0) volumetrically by marching camera rays through `bounds`=(min_corner, max_corner) and accumulating the volume-rendering integral. +- `png_bytes(rgb01, level=6)` -- Encode an (H,W,3) image in [0,1] to PNG *bytes* -- a minimal, pure-stdlib encoder (zlib + struct), so the render module carries no image-library dependency. +- `save_png(path, rgb01, level=6)` -- Write an (H,W,3) image in [0,1] to a PNG file. +- `frame_delta_tiles(prev, curr, tile=32, thresh=0.001)` -- The pixel-streaming primitive: split two frames into `tile`x`tile` blocks and return only the tiles that CHANGED, as a list of (row, col, tile_pixels). + +### `holographic_pipeline` +*holographic_pipeline.py -- ONE configurable render/simulation pipeline: pick a preset (or set flags), see exactly which stages will run and WHY, and get the right pipeline every time -- with the hand-assembly path still there for full control.* + +- **class `PipelineError`** -- Raised at BUILD time (not render time) for an impossible configuration -- with a message that says what is wrong, so you fix the config instead of debugging a wrong image later. +- **class `Stage`** -- One composable step. +- **class `FrameState`** -- The mutable bag stages thread through: a stage's output is the next stage's input. +- **class `RenderSpec`** -- A REAL scene for the pipeline (backlog A1). +- **class `PipelineConfig`** -- Everything you'd otherwise piece together by hand, in ONE place -- the single opt-in/out surface. + - `preview()` -- Fast, interactive-looking: reuse+reproject last frame, SVGF denoise, a splat proxy. + - `final()` -- High quality single frame: full samples, no dirty-only reuse, final grade. + - `interactive()` -- Preview plus live simulation (fluid + collision + field effects). + - `ocean()` -- Preview plus the adaptive ocean/wave FX -- the plan_waves dispatch runs the cheap spectral method almost everywhere and the dear breaking grid solver only in the tiles that actually break. +- `dispatch_render(ctx)` -- Resolve the RenderSpec's render method (or pick one via 'auto'), CHECK the chosen strategy's needs are present, then run it. +- `build_pipeline(cfg, registry=None)` -- Config -> ordered, validated Pipeline. +- **class `Pipeline`** -- An ordered list of stages. + - `plan(self)` -- Dry run / EXPLAIN: every active stage, WHY it is here, and what it NEEDS and PRODUCES -- without rendering. + - `stage_names(self)` -- + - `run(self, scene=None, seed=0, prev_frame=None, renderer=None)` -- Execute the pipeline: build the shared FrameState and thread it through every stage in order. + - `lower_to_program(self, machine)` -- Phase 6: LOWER the pipeline to a machine PROGRAM -- one APPLY instruction per stage, in order, then HALT. + - `run_on_vm(self, machine=None, scene=None, seed=0, prev_frame=None, renderer=None)` -- Phase 6: RUN the pipeline ON the VM instead of a Python for-loop. + +### `holographic_session` +*holographic_session.py -- ONE render session that ties the disconnected rendering threads together.* + +- `sdf_surface_points(sdf, bounds, n=2000, seed=0, eps=0.02, oversample=8)` -- Sample points that lie ON an SDF's surface -- the front half of the SDF->splat bridge that was missing. +- **class `RenderSession`** -- One scene, every renderer. + - `preview(self, width=None, height=None, **kw)` -- FAST path: the material preview via render_surface (Lambert + spec + env reflection + one transparency layer), resolving every SurfaceMaterial channel per hit. + - `render_final(self, spp=64, on_progress=None, progress_every=8, width=None, height=None, max_bounce=4, sky=None, seed=0, should_stop=None)` -- SLOW path: the photoreal final via path_trace, using the SAME SurfaceMaterials as the preview (through the material adapter). + - `to_splats(self, n=2000, radius=0.12, seed=0)` -- PROXY path: sample the SDF surface and fit splats (field_to_splats) so the scene can be drawn by a lightweight browser billboard shader -- no three.js scene graph, no mesh pipeline. + - `set_material(self, obj_id, material)` -- Replace one object's material. + - `edit_channel(self, obj_id, channel, value)` -- Edit ONE channel of one object's material (colour/roughness/reflect/emission/opacity) -- the value can be a constant, a Param, a pattern field, or a map. + +### `holographic_cancel` +*holographic_cancel.py -- COOPERATIVE CANCELLATION for long operations (modeling-app backlog, item F).* + +- **class `CancelToken`** -- A cooperative cancel flag. + - `cancel(self)` -- Request cancellation. + - `reset(self)` -- Clear the flag so the token can be reused for the next operation. + - `cancelled(self)` -- + - `should_stop(self)` -- The cooperative check a loop calls between chunks. +- `run_cancellable(iterable, token, on_step=None)` -- Iterate `iterable`, yielding items until `token` is cancelled -- a thin helper for wrapping any step loop (a sim advancing frames, an iterative solver) in cancellation. + +## Export / LOD + +### `holographic_lod` +*Screen-space-error level-of-detail policy (holographic_lod).* + +- `build_lod_chain(mesh, targets=(0.5, 0.25, 0.125))` -- Decimate `mesh` (via QEM) to a chain of coarser levels at the given face-count FRACTIONS of the original, measuring each level's surface deviation from the ORIGINAL mesh. +- `build_cluster_lod_chain(mesh, grids=(48, 24, 12))` -- The PARALLEL counterpart of build_lod_chain, for an IMPORTED mesh with no field behind it: vertex-cluster the mesh (cluster_decimate) at decreasing grid resolutions, measuring each level's surface deviation from the ORIGINAL. +- `screen_space_error(world_error, distance, screen_height_px=1080, fov_rad=...)` -- Project a world-space error to screen pixels at a viewing distance. +- `select_lod(chain, distance, pixel_threshold, screen_height_px=1080, fov_rad=...)` -- Index of the COARSEST level in `chain` whose MAX screen-space error is still under `pixel_threshold` at this distance -- the cheapest mesh that looks right. + +### `holographic_gltf` +*Binary glTF (.glb) emission and parsing (FWD-2): the boundary between the NumPy back end and three.js.* + +- `mesh_to_glb(mesh, base_colour=(0.8, 0.8, 0.8, 1.0), generator='holostuff', material=None)` -- Serialise a `Mesh` to a single-file binary glTF (`.glb`) and return the bytes. +- `glb_to_mesh(data)` -- Parse a binary glTF (`.glb`) back into a `Mesh`. +- `write_glb(mesh, path, **kw)` -- Write a mesh to a `.glb` file. +- `read_glb(path)` -- Read a mesh from a `.glb` file. +- `validate_glb(data)` -- A structural conformance check on a `.glb`: the container a real GLTFLoader requires. diff --git a/CAPABILITIES.md b/CAPABILITIES.md new file mode 100644 index 00000000..54cba813 --- /dev/null +++ b/CAPABILITIES.md @@ -0,0 +1,758 @@ +# leCore Capabilities + +*A plain-language menu of what leCore can do and how to start -- auto-generated by `capdoc.py` from the engine's own capability catalog. For the full module reference see REFERENCE.md; for the app-building API surface see API_QUICKREF.md.* + +Every entry below is a **capability home**: a job the engine already solves, the one call that gets you started, and the words you can search it by. You don't have to read this list -- the engine can find the right home for you at runtime: + +```python +import lecore +mind = lecore.UnifiedMind() +mind.find_capability('search a big pile of vectors') # -> the best-matching homes +mind.suggest('edit an image') # -> homes + a confidence + the call +mind.route('render a scene') # -> 'act' with the call, or 'choose' options +``` + +Or over HTTP, once you run the service (see SERVICE.md): `GET /skills`, `POST /skills/suggest`, `POST /skills/route`. + +## Core algebra & datatypes + +*the five primitives everything is built from -- bind, bundle, cleanup -- and the vector datatype itself.* + +### Compute (VSA-native) +stay in the vector/frequency domain with no Python hops: FUSE a bind/bundle/permute chain into ~2 FFTs (measure the FFT drop), the fuse-runs SCHEDULER, width, and running logic as a VSA PROGRAM. Rule: push decisions/cleanups to the boundaries. + +```python +from holographic_computehome import Compute; Compute.fuse_record(keys, values) +``` +*Find it by:* compute, fuse, fused, schedule, execute, program, machine, fft + +### Distributed compute across machines (farm) +run the same partition-and-reduce work across a FARM of machines. Each node runs holographic_coordinator.serve_worker(workers={name: fn}); mind.farm(['host1:9000','host2:9000'], token).run(buckets, worker_name, cache, reduce) round-robins the buckets across nodes and reassembles by the monoid reducer -- the same call as the local pool, just cross-machine. SAFE by design: workers run BY NAME (a node only runs workers it registered), so no code crosses the wire, only data. stdlib sockets/JSON.. + +```python +from holographic_coordinator import serve_worker; serve_worker(port=9000, workers={'sum': fn}) # then: mind.farm(['host:9000'], token).run(buckets, 'sum', None, reduce_sum) +``` +*Find it by:* farm, distributed compute, cluster, network farm, worker node, serve_worker, render farm, compute across machines + +### Encoders (number to vector) +turn raw values into hypervectors: scalar & fractional-power encoding (encoders/fpe -- nearby numbers map to nearby vectors), N-D coordinate fields (fpefield), complex-phasor FHRR (fhrr), sparse block codes (sbc), geometric-algebra Clifford (clifford), and exact integer arithmetic over phasors (rns). How data ENTERS the substrate. + +```python +from holographic_encoders import ScalarEncoder; from holographic_fpe import ... +``` +*Find it by:* encode, encoder, number to vector, scalar encoding, fractional power encoding, fpe, encode coordinates, phasor + +### Hypervector (datatype) +the first-class hypervector: a raw vector + its dim / encoder / tag, with the five verbs (bind/unbind/bundle/cleanup/permute) as methods. Encoders are the constructors; the raw array stays one attribute away (.array / np.asarray(hv)). + +```python +from holographic_hypervector import Hypervector; Hypervector.encode(encoder, value).bind(other) +``` +*Find it by:* hypervector, datatype, vector, vsa, hdvector, symbol, bind, bundle + +### Transform (warp) +move / rotate / warp across representations: VSA bind (rigid) + permute (order), 4x4 matrices (translate/scale/rotate/compose/decompose/look_at + quaternions), clifford rotors, anisotropic steering -- one facade. + +```python +from holographic_transformhome import Transform; Transform.translation(t) +``` +*Find it by:* transform, warp, rotate, translate, scale, rigid, affine, matrix + +### kernel verbs +the five primitives: bind (attach/transform), unbind (query), bundle (superpose/blend), permute (order), cleanup (recognise/denoise). + +```python +from holographic_ai import bind, bundle; from holographic_ai import Vocabulary # Vocabulary(...).cleanup(x) +``` +*Find it by:* bind, unbind, bundle, cleanup, permute, superpose, blend + +## Discover & drive it (for agents) + +*let the engine describe and route ITSELF -- suggest a capability for a task, autocomplete, skill cards.* + +### Agent skills (discover & route) +the AGENT-FRIENDLY layer: mind.skills() lists every capability + method with how to CALL it (skill descriptions, real signatures); mind.suggest(task) ranks capabilities for a plain-English task WITH a confidence + the call; mind.route(task) is a decision node ('act' with the call when confident, else 'choose' the options); mind.complete_method(prefix) autocompletes method names; mind.describe_skill(name) is a skill card. Also over HTTP: GET /skills, POST /skills/suggest|route|complete|card. + +```python +mind.route('render a scene'); mind.suggest('edit an image'); mind.complete_method('learn_') +``` +*Find it by:* agent, agentic, skills, skill description, autocomplete, suggest, decision tree, route + +### Who's online (presence registry) +mind.registry tracks live actors: announce(principal) is a heartbeat, registry.list(kind=, workspace=) discovers who's here, is_online() checks one, and an actor that stops heart-beating for `ttl` seconds drops out on its own. Rides the mind's bus so presence is visible across nodes -- how a swarm or farm finds its peers.. + +```python +mind.registry.announce(agent); online = mind.registry.list(kind='agent'); mind.registry.is_online(agent) +``` +*Find it by:* registry, presence, who is online, heartbeat, discover peers, list agents, who's connected, liveness + +## Memory, search & recall + +*store things and get them back by CONTENT, not by exact key.* + +### Adaptive record (load-gated) +a role->filler memory that picks its representation by LOAD and FIDELITY need -- cheap real-HRR at low load, FHRR phasors past the capacity knee, or tensor-product binding for EXACT recall (perfect to M~dim, at dim*dim storage). Uniform add/recall; deciders are exact integers/flags, no harm mode on recall. + +```python +from holographic_loadmemory import AdaptiveRoleFillerMemory; m=AdaptiveRoleFillerMemory(dim, pairs, exact=True) +``` +*Find it by:* adaptive record, role filler memory, fhrr, phasor, tensor, exact recall, load, capacity + +### Cache (bake-and-query) +bake a slow evaluator over what VARIES (position/view/time/constant) then look it up cheaply -- one shared grid-sample core over the scattered bakes (matbake, sdfbake, viewlut, anim). + +```python +from holographic_cachehome import Cache; Cache.bake(fn, vary='position', lo=lo, hi=hi, res=24) +``` +*Find it by:* bake, precompute, lookup, cache, memoise, irradiance, lut, grid + +### Cold storage (compress inactive data) +shrink INACTIVE data to save memory and disk, and inflate it back on demand: store = mind.cold_store(keep_warm=8) keeps only the K most-recently-used values live and compresses the rest, warming any of them transparently on get(); mind.cool(big_table) wraps ONE value so c.cool() frees its RAM and c.get() brings it back bit-identical. Works on tables, whole databases, big arrays, any picklable structure; codec='lzma' packs smaller, spill_dir=... writes cold blobs to disk. Honest: high-entropy VSA vectors barely compress (the win there is freeing the live object / spilling to disk); redundant/text/structured data compresses a lot. The query Database can auto-cool its own idle tables: db.enable_cold_storage(keep_warm=K) then db.cool_idle() compresses tables you haven't queried lately and a query warms them back -- and a DB shipped to a distributed worker arrives warm + cooling-off, so a shared read-only cache is never mutated.. + +```python +store = mind.cold_store(keep_warm=4); store.put('t1', big_table); store.get('t1') # transparently warmed +``` +*Find it by:* cold storage, compress inactive, evict, spill to disk, cool, warm, fold up, shrink memory + +### File map ingest (folder / zip -> queryable) +point at a FOLDER, a .zip, or a file and digest it into a queryable FILE MAP: fm = mind.ingest_files('project/') (or 'bundle.zip'). Query it by NAME/glob (fm.find('*.png')), KIND (fm.by_kind('model'): image/text/model/data/code/archive), METADATA (larger_than/newer_than/by_ext), text CONTENT (fm.search_text('shader normal') -- an inverted index over the text files), and MEANING (fm.build_meaning_index() then fm.find_by_meaning('lighting')). fm.tree() is the folder hierarchy. Every file is also tracked for RELOCATION/CHANGE (fm.missing()/changed()/relink(one,new)/resolve_assets(roots)), so a moved/edited tree self-heals. Stdlib only; text indexing is size-capped.. + +```python +fm = mind.ingest_files('my_project.zip'); fm.find('*.obj'); fm.search_text('normal map'); fm.tree() +``` +*Find it by:* ingest, ingest files, index a folder, digest a folder, read a zip, scan folder, file map, make files queryable + +### Index (search) +nearest-neighbour / recall over a pile of vectors with ONE interface (Index.nearest(q,k)): exact cosine scan for small sets, sub-linear RP-forest for large, plus a calibrated abstain. + +```python +from holographic_index import Index; Index(vectors, labels=names).nearest(query, k=5) +``` +*Find it by:* knn, nearest, lookup, recall, retrieve, similarity, search, index + +### Memory (cache hierarchy) +keep the hot working set where the CPU can reach it fast: FFT spectrum residency (skip recomputing a reused transform), batched contiguous bind (one FFT for a whole record), tiling to fit a cache level, and the opt-in GPU / numba backends. + +```python +from holographic_memoryhome import Memory; Memory.bind_cached(a, b, cache) +``` +*Find it by:* memory, cache, residency, resident, spectrum cache, batch, bind_batch, backend + +### Semantic word index (find words by meaning) +the fuzzy REVERSE of a dictionary: describe an idea and get the words whose definitions mean it. mind.build_semantic_index(words=...) places words in a meaning space by RANDOM INDEXING over their glosses, then idx.find('unexpected good luck') -> 'serendipity' and idx.similar('puppy') -> 'dog','kitten'. OPT-IN and separate: nothing loads or builds until you call it. Approximate by design (this is where leCore's geometry-preserving/lossy side belongs) -- reliable for the top hit, noisy in the tail, and word-sense sensitive.. + +```python +idx = mind.build_semantic_index(words=my_vocab); idx.find('a young dog'); idx.similar('ocean') +``` +*Find it by:* semantic index, find words by meaning, reverse dictionary, words like, similar words, meaning search, word similarity, describe a word + +## Geometry, modeling & rendering + +*build shapes (mesh or SDF), texture and light them, and render to an image.* + +### 2D image editing & generation +the engine's 2D IMAGE toolkit: edit (recolor_image / colour transfer, sharpen_loop, svgf_denoise, downscale), generate & blend (blend_images crossfade/morph, pattern_field procedural noise/fbm/checker/stripes, svg_canvas vector drawing), store & compare (image_archive damage-tolerant recall, compare_images / image_distance perceptual similarity). Raster and vector, all on the VSA substrate. + +```python +mind.recolor_image(img, ref); mind.blend_images(a, b); mind.pattern_field('fbm'); mind.svg_canvas() +``` +*Find it by:* 2d, image, edit an image, generate an image, draw, draw a picture, make a drawing, paint + +### Adaptive rendering +the render call that picks its own methods/quality: the converging sampler that stops per-pixel when the confidence interval is tight, and the render-method auto-picker. + +```python +from holographic_gbuffer import render_auto, converge_samples +``` +*Find it by:* adaptive, auto, quality, converge, raytracing mode, render mode + +### Asset relocation / relink (external files) +track the EXTERNAL files a scene depends on (textures, models, ...) and repair their paths when they move -- the '3-D missing textures' problem. lib = mind.asset_library(); lib.add(path); then when a folder moves, lib.relink(one_asset, its_new_path) re-finds every OTHER moved file automatically (it works out the moved parent and rewrites the rest, then structurally SEARCHES for anything reorganised). lib.changed() spots files edited on disk (size/mtime or content hash); lib.search_under(folder) finds missing files under a folder; lib.resolve(asset, roots=) locates a file by CONTENT HASH across machines (the distributed fallback). Saves/loads a JSON manifest.. + +```python +lib = mind.asset_library(); lib.add('project/textures/water/wave.png'); lib.relink(lib.assets[0], 'newroot/project/textures/water/wave.png') +``` +*Find it by:* asset, assets, relink, relocate, missing textures, broken path, fix paths, external files + +### Field +sample a scalar/vector field at points with ONE interface (field.sample(points)); the backend is chosen by cost: callable/oracle, dense grid, narrow-band sparse (spectral/FPE/region/dirty are backends too). + +```python +from holographic_fieldhome import Field; Field.grid(arr, lo, hi).sample(pts) +``` +*Find it by:* field, grid, volume, density, sdf, sample, voxel + +### Geometry (domain) +build and edit shapes three ways: explicit MESH (half-edge + verbs), implicit SDF (CSG + raymarch), and SPLATS (Gaussian clouds) -- convertible via meshbridge. + +```python +from holographic_mesh import Mesh; from holographic_sdf import box, sphere +``` +*Find it by:* geometry, mesh, sdf, splat, shape, model, csg, subdivide + +### Import artist file formats (OBJ/glTF/textures/volume) +import the files artists hand you: mind.load_obj('model.obj') reads Wavefront geometry + its .mtl (UVs, normals, per-face material, map_* textures); mind.load_glb('model.glb') reads glTF/GLB geometry AND its full PBR channels (base colour / metallic-roughness / normal / occlusion / emissive) with embedded textures and per-vertex UVs/normals, AND for rigged models its ANIMATIONS (keyframed node transforms -- clip.sample(t), rotations slerped) and SKINS (joints + inverse-bind + weights); mind.load_texture_set(folder) turns a folder of Adobe Substance 3D Painter export maps (basecolor/roughness/metallic/normal/height/ao/emissive, matched by name) into one PBRMaterial; mind.load_volume('grid.npy') wraps a 3-D density grid as a field for render_volume. mind.import_asset(path) dispatches by extension. Once a rigged glTF is loaded, mind.deform_mesh(loaded, clip, t) actually MOVES it -- linear-blend skinning by the animated skeleton plus morph-target blending, returning the deformed mesh at time t. Stdlib+NumPy; PIL lazy for textures. HONEST: proprietary .sbsar/.spp and sparse OpenVDB .vdb need their vendor tools -- import the exported open forms.. + +```python +lm = mind.load_obj('chair.obj'); glb = mind.load_glb('robot.glb'); mat = mind.load_texture_set('exports/brick'); vol, b = mind.load_volume('smoke.npy') +``` +*Find it by:* import, load obj, load gltf, load glb, mtl, wavefront, substance painter, adobe painter + +### Instancing (shared definition + type-safe binding) +place ONE shared definition many times so editing it once updates every copy (edit-once): mind.shared_definition('chair', mesh, 'metal') then scene.place(defn, transform) in mind.instanced_scene(); repaint the definition and all instances change. The material<->geometry binding is TYPE-CHECKED at compose time -- a surface material only binds to a mesh, a volumetric one (fog/smoke/fire) only to a volume -- so a bad binding is refused, not rendered wrong. flatten_surface() materialises the surface instances into one mesh. CMP4. + +```python +chair = mind.shared_definition('chair', box_mesh, 'metal'); s = mind.instanced_scene(); s.place(chair); chair.set_material('glass') +``` +*Find it by:* instance, instancing, shared definition, edit once, duplicate, reuse geometry, material binding, surface volume + +### Layered material (order schema) +an ORDERED stack of material layers -- base -> diffuse -> specular/reflection -> coat/clearcoat -- where the order is a SCHEMA checked at compose time, so you can't put a reflection under a diffuse (an out-of-order stack is refused up front). Each layer composites OVER the one below by a coverage alpha (a number, field, or texture graph). Honest: fixes the stacking, not the energy-conserving radiometry of a true layered BRDF. CMP2. + +```python +mind.layered_material([mind.material_layer('base', paint), mind.material_layer('clearcoat', gloss, alpha=0.3)]).sample('albedo', [0.3, 0.7]) +``` +*Find it by:* layered material, material layers, clearcoat, coat, layer stack, material stack, over compositing, base diffuse specular coat + +### Lighting (domain) +one home for lighting: the light types (point/directional/spot/area/dome/IES) and the shade INTEGRAL in each mode -- direct NEE, PRT relight, environment SH; render methods call it. + +```python +from holographic_lightinghome import Lighting, RectLight +``` +*Find it by:* lighting, light, lamp, shadow, dome, area, ies, spot + +### Material (channels) +the material as a record of named channels (albedo/metallic/roughness/normal/...) you sample per point; its position-dependent channels BAKE via the Cache home and shade via the Shading home. + +```python +from holographic_material import Material +``` +*Find it by:* material, channels, albedo, roughness, metallic, shader + +### Material library (render + physical) +the engine's material LIBRARIES, discoverable in one place: ~141 RENDER presets (metals/gems/woods/stones/liquids/biomes -- PBR appearance) and ~120 PHYSICAL materials in 12 categories (metals/liquids/gases/polymers/ceramics/glass/minerals/stone/wood/biological/building/semiconductors) with density, refractive index, viscosity, Young's modulus, sound speed, specific heat, thermal conductivity/expansion, melting/boiling point, phase -- validated, unit-documented, for solvers/scientists. material_info(name) gives BOTH how a material looks AND how it behaves; find_materials()/materials() search + list across both. Users can add their own to either library. + +```python +mind.material_info('gold'); mind.find_materials('clear liquid'); mind.materials() +``` +*Find it by:* material library, materials, physical material, material properties, density, refractive index, render material, pbr preset + +### Mesh editing (DCC) +modeling/DCC edits on a Mesh: extrude/inset faces (meshpoly), subdivide + smooth (meshsubdiv, Catmull-Clark), deform/warp (deform), rig-skin-pose a skeleton (blendpose), UV unwrap (chart), decimate/QEM, booleans, and mesh<->SDF. Blender-parity polygon editing. + +```python +mind.deform(mesh, ...); mind.mesh_to_sdf(mesh); from holographic_meshverbs import extrude_face +``` +*Find it by:* edit a mesh, extrude, bevel, inset, subdivide, smooth a mesh, decimate, reduce polygons + +### Multi-material (mask-blended) +combine N materials by per-point MASKS -- generalises the 2-way Material.blend to a weighted mix where each material's weight is a mask (a texture graph, a field, or a constant) that varies over the surface: paint rust into metal, moss onto stone, a decal onto a surface. 'blend' = soft weighted sum (weights normalised so brightness stays put); 'select' = hard pick the dominant material (a material-ID / splat map). CMP3. + +```python +mind.multi_material([metal, rust], [1.0, mind.texture_leaf('fbm', n_dims=2)]).sample('albedo', [0.3, 0.7]) +``` +*Find it by:* multi-material, multimaterial, blend materials, material mask, material map, splat map, material id, paint materials + +### Multi-scatter BRDF (re-enable) +energy-conserving GGX for rough metals: the Kulla-Conty multi-scatter term adds back the energy single-scatter GGX loses (white-furnace ~0.4 -> ~1.0 at high roughness), GATED by roughness so smooth surfaces skip it (the term overshoots at low roughness). Detector is the exact material roughness. + +```python +from holographic_brdf import brdf_gated, cook_torrance_ms; brdf_gated(N,V,L,color,metallic,roughness) +``` +*Find it by:* multi-scatter, multiscatter, kulla-conty, energy conservation, brdf, ggx, rough metal, white furnace + +### Network render farm +run the coordinator's monoid workers on OTHER machines: a worker daemon per node (stdlib http/json), the read-only cache shipped ONCE by content hash and reused, buckets dispatched concurrently and reduced -- the same Coordinator.run as the local pool. Buckets are data, workers are registered code; a node runs only its registered workers. + +```python +from holographic_farm import WorkerDaemon, NetworkFarm; Coordinator(NetworkFarm([addr])).run(buckets, 'worker_name', cache, reduce) +``` +*Find it by:* render farm, distributed, network, seti, worker daemon, remote, cluster, node + +### Pipeline (render/sim) +compose a render or sim run as ordered stages that declare what they need/produce; dispatch among render strategies (pathtrace/raymarch/prt/radiance) and catch a missing input before running. + +```python +from holographic_pipeline import build_pipeline, PipelineConfig, RenderSpec +``` +*Find it by:* pipeline, stage, compose, run, render, strategy, dispatch, route + +### Preview (swatch & material ball) +SEE what you composed: mind.preview_texture(graph) renders a CMP1 texture graph as a flat RGB swatch, and mind.preview_material(material) renders a material on the classic MATERIAL BALL sphere (Cook-Torrance shaded, using the material's roughness/metallic channels) -- works on a plain Material or a CMP2/CMP3 layered/multi material. Returns a float image in [0,1] to save/view. The missing step between composing a texture/material and looking at it.. + +```python +img = mind.preview_texture(graph); ball = mind.preview_material(layered_material) +``` +*Find it by:* preview, swatch, material ball, material preview, texture preview, see the texture, render swatch, thumbnail + +### Render graph (bake vs live) +the PIPELINE composing the texture/material/scene graphs: mind.render_graph() registers texture graphs (static or dynamic) + a CMP4 instanced scene, then plan() shows what it will do and WHY and prepare() runs it. The adaptive decision it adds is BAKE a static texture graph to a grid (O(1) bilinear lookup, mind.bake_texture) vs SAMPLE it live -- baking amortises a deep graph over many hits, live avoids re-baking a changing map every frame. Trade: memory + interpolation error. CMP5. + +```python +rg = mind.render_graph(); rg.add_texture('rust', graph, static=True).set_scene(scene); rg.plan(); prep = rg.prepare() +``` +*Find it by:* render graph, bake texture, bake vs live, prepare scene, resolve textures, orchestrate render, material lod, precompute texture + +### Rendering (path trace) +render a scene to an image: path_trace (Monte-Carlo global illumination), a camera controller, indirect-light gather + irradiance cache (globalillum), precomputed radiance transfer (prt), volumetric integration, and lens/DOF + post-FX. The analysis-by-synthesis render path. + +```python +mind.path_trace(scene); mind.camera(); from holographic_raymarch import sphere_trace +``` +*Find it by:* render a scene, path trace, ray tracing, global illumination, camera, depth of field, lens, volumetric render + +### SDF & procedural geometry +implicit + procedural geometry: signed distance fields (sdf), sphere-trace raymarching with ambient occlusion (raymarch), sculpting, procedural terrain (procgen), spatial tiling + octree, and voxelization. Native-first shape building. + +```python +from holographic_raymarch import sphere_trace; mind.terrain(...); from holographic_sdf import ... +``` +*Find it by:* sdf, signed distance field, raymarch, sphere trace, sculpt, procedural terrain, procedural geometry, voxelize + +### Shading (BRDF) +the shade model: cook_torrance (full specular+diffuse per light), lambert (diffuse term), sample_brdf (importance-sampled bounce) -- call these instead of re-deriving Fresnel/GGX/diffuse. + +```python +from holographic_brdf import cook_torrance, lambert +``` +*Find it by:* shade, brdf, cook_torrance, lambert, fresnel, ggx, specular, diffuse + +### Shadow / visibility (domain) +test whether light or the environment reaches a point: SDF soft shadow (Quilez penumbra), ambient occlusion, hard shadow-ray (NEE), and PRT baked visibility -- one home of strategies render paths call. + +```python +from holographic_shadowhome import Shadow; Shadow.soft(sdf, P, Ldir) +``` +*Find it by:* shadow, visibility, occlusion, ambient occlusion, penumbra, shadow ray, soft shadow, unoccluded + +### Splat aniso-refine (re-enable) +full-3DGS anisotropic refinement composed coarse-first: fit cheap isotropic splats, then gradient-refine the RESIDUAL (what iso missed -- sharp / oriented features) with anisotropic Gaussians. Strictly >= the isotropic baseline (no harm mode); big win on sharp edges. Opt-in (no reliable cheap detector for WHEN it pays). + +```python +from holographic_splat import fit_coarse_first; fit_coarse_first(target, K_iso, K_aniso) +``` +*Find it by:* splat refine, anisotropic splat, 3dgs, gaussian splat, coarse first splat, aniso fit, residual refine, gradient refine + +### Texture (domain) +procedural + example-based surface detail as FIELDS you plug into a Material channel: fbm noise, Voronoi/cellular cracks, divergence-free curl, patch synthesis; plus the weathering set (burn/oxidation/inclusions). + +```python +from holographic_texturehome import Texture; Param(field=Texture.voronoi(kind='edge')) +``` +*Find it by:* texture, noise, fbm, voronoi, curl, procedural, weathering, pattern + +### Texture graph (composable maps) +build a texture as a TREE of maps: an op (mix/multiply/over/scale/remap/...) over TYPED inputs -- map | color | field | number -- each of which may be another map, so graphs nest to any depth. Sampling walks the tree; the input types are checked at COMPOSE time so a bad graph (a colour used as a weight, a missing input) is refused up front, not rendered wrong. Encode a graph to a hypervector to cache/search it. CMP1. + +```python +mind.texture_op('mix', a=mind.texture_leaf(value=[1,0,0]), b=mind.texture_leaf(value=[0,0,1]), t=mind.texture_leaf('fbm', n_dims=2)); mind.sample_texture(g, [0.3,0.7]) +``` +*Find it by:* texture graph, map graph, shader graph, compose texture, layered texture, node graph, blend maps, mix textures + +### Textured object render (paint composed maps) +paint a COMPOSED texture or material (CMP1 graph / CMP2-3 material) onto an object and render it: mind.render_textured(scene, {object_name: texture_graph}) marches the scene, UV-wraps each texture onto its object (spherical map on a sphere, planar on a box), and shades with the real Cook-Torrance BRDF + a light + a hard shadow. This is the composability stack driving a full 3-D render, not just a swatch. Honest: textbook UV (seams), single hard light.. + +```python +tex = mind.texture_op('mix', a=mind.texture_leaf(value='orange'), b=mind.texture_leaf(value='purple'), t=mind.texture_leaf('fbm', n_dims=2)); mind.render_textured(scene, {scene.names()[0]: tex}) +``` +*Find it by:* textured render, paint texture on object, wrap texture, uv render, texture the sphere, composed texture render, map onto object + +## Scenes you can describe & adjust + +*talk a 3-D scene into being, then adjust its named objects in words, and render or simulate it.* + +### Scene from description (semantic) +DESCRIBE a 3-D scene in plain words and the engine builds it, then you ADJUST it by talking to named objects: mind.build_scene('a big red metal sphere and a small blue glass box on a sunny day') returns a live SemanticScene; then scene.adjust('make the sphere bigger'), scene.adjust('change the box to metal'), scene.set('the red sphere', material='glass'), scene.render(), scene.simulate(). NAME objects to reference them easily -- scene.name('the red sphere', 'hero') or scene.adjust('call the box crate'), then scene.adjust('make hero glass'); scene.rename('hero','champion'). PAINT a procedural TEXTURE by talking to it -- scene.adjust('give hero a rusty texture'), scene.paint('crate', 'marbled') (rusty/marbled/mossy/cloudy/lava/striped/noisy) -- and scene.render() paints it on. Attach an EXTERNAL image file as a texture -- scene.attach_texture_file('the sphere', 'project/textures/wave.png') -- and the scene tracks it in an AssetLibrary: if the files move, scene.set_asset_roots([...]) + scene.resolve_assets() (or scene.relink(one, new)) re-find them and render() reloads them, falling back to the object's colour if one is missing. When a command is unclear it SUGGESTS rather than fails -- scene.interpret(cmd) previews what it understood + 'did you mean?' hints, scene.options() lists what you can say, scene.feedback holds the last report. Or wrap an existing object list with mind.semantic_scene(objects). Controlled vocabulary, deterministic. + +```python +scene = mind.build_scene('a red metal sphere and a blue box'); scene.name('the sphere','hero'); scene.adjust('give hero a rusty texture'); scene.render() +``` +*Find it by:* scene, describe a scene, build a scene, make a scene, create a scene, scene from text, 3d scene, adjust the scene + +## Simulation & physics + +*step a solver forward -- fluids, smoke, cloth, soft bodies, collisions, reaction-diffusion.* + +### Physics & chemistry (domain) +physical/chemical PROPERTIES and their evolution: the matter model (Mixture/matter_step: smoke->oil separation), diffusion, equilibrium propagation, thin-film iridescence, oxidation/weathering. + +```python +from holographic_mixture import Mixture, matter_step +``` +*Find it by:* physics, chemistry, matter, mixture, diffusion, material properties, iridescence, oxidation + +### Simulation (domain) +a shared STEP LOOP over any solver (fluids/smoke, fire/combustion, softbody/cloth, hair, MPM, collision, reaction-diffusion) -- each keeps its own math; the scaffold gives them one step(dt) and exposes their field for the Pipeline to render. + +```python +from holographic_simulationhome import Simulation; Simulation.for_fluid(fluid).run(10) +``` +*Find it by:* simulation, solver, fluid, smoke, fire, cloth, softbody, step + +## Language, knowledge & text + +*generate text, teach the engine language, and look words up in a real vendored dictionary.* + +### Dictionary + taxonomy (vendored) +a comprehensive vendored English DICTIONARY (~144k words: definition, part of speech, synonyms, example) AND an is_a TAXONOMY (encyclopedia side: 'a dog is a kind of domestic animal...'), giving the engine real world-knowledge for contextual awareness beyond its internal machinery. OPT-IN + lazy: it never loads from importing leCore or building a mind -- only the first language call decompresses it (lzma, ~3.3 MB on disk) into a plain dict in RAM (~22 MB), after which lookups are instant. Control it explicitly with holographic_dictionary.is_loaded()/preload()/unload()/stats(). Stdlib-only (lzma+json); the mind can also LEARN meaning from it. Princeton WordNet, free with attribution. + +```python +mind.lookup('gravity'); mind.word_taxonomy('dog'); import holographic_dictionary as hd; hd.stats() +``` +*Find it by:* dictionary, define, definition, word meaning, synonyms, encyclopedia, taxonomy, is a + +### Language learning +TEACH the mind language natively: read (read a corpus), learn_dictionary / learn_vocabulary (word meaning from definitions -- including the vendored dictionary), learn_encyclopedia (relational facts + is_a taxonomy), and learn_sequence (order/grammar). The language CURRICULUM -- definitions, then facts, then reading. + +```python +mind.read(corpus); mind.learn_vocabulary(words); mind.learn_encyclopedia(facts) +``` +*Find it by:* learn from a corpus, train on text, teach the model, teach language, language curriculum, learn word meanings, learn a language, read a corpus + +### Text generation +GENERATE text on the VSA substrate: generate(seed, length, temperature) and generate_structured (n-gram / beam), respond(query) for a query-conditioned reply, and answer(question) / answer_text for factual answers. The engine's write-a-sentence faculties. + +```python +mind.generate('once upon a', length=120); mind.respond('describe a sunset'); mind.answer('what is gravity') +``` +*Find it by:* generate text, write, write a sentence, write a paragraph, text generation, compose text, respond, reply + +## Learning & agents + +*gradient-free learners and agents -- an RL creature, a classifier, a reservoir, mixtures of experts.* + +### Learning & agents +gradient-free learning on the substrate: an RL agent with a value head + drives (agent), a holographic classifier, an echo-state reservoir (reservoir), mixture-of-experts (moe), KAN, forward-forward, recurrent/predictive nets, and dreaming. NPC brains and on-line learners with NO autodiff. + +```python +mind.agent(...); mind.classify(x); mind.reservoir(...) +``` +*Find it by:* reinforcement learning, rl agent, train a classifier, classify, policy, npc brain, game ai, reservoir + +### Message bus + agent (LLM) bridge +connect a person AND an agent to the running tool at once, and let the app PUSH to the agent instead of the agent polling: mind.bus() is a message bus (publish/subscribe by topic, mailboxes to pull an inbox, history); mind.run_task('render', fn, background=True) runs a job and publishes 'render.done' with a small summary when it finishes; mind.agent_bridge(llm=my_fn).notify_on('render.done', 'does it look right?') calls YOUR llm (any text->reply callable -- no LLM library is imported, so it's fully optional) and posts the reply on the bus. Over HTTP a remote agent uses /bus/publish + /bus/poll. The LLM is optional; leCore runs with no agent attached.. + +```python +bridge = mind.agent_bridge(llm=my_llm); bridge.notify_on('render.done', 'does it look right?'); mind.run_task('render', lambda: scene.render(), background=True) +``` +*Find it by:* message bus, event bus, pubsub, publish subscribe, agent bridge, llm bridge, notify the agent, push notification + +### Scoped identity for any actor (Principal) +mind.principal(id, workspace, kind) gives an agent, user, service, or peer leCore instance ONE scoped identity where isolation is the default: a private database namespace (it writes only there), a directed inbox topic (it reads only its own messages, sender-stamped), a provenance role that tags everything it contributes (holographic_provenance.source_role / from_external), and an optional private learning overlay. Signals and state can't cross between principals -- so multiplayer workspaces, agent swarms, and guest peer nodes are the same isolation solved once.. + +```python +alice = mind.principal('alice', workspace='lab', kind='user'); alice.send(mind.bus(), to='bob', payload={...}); alice.poll(mind.bus()) +``` +*Find it by:* principal, identity, scoped identity, per-agent state, per-user namespace, multiplayer, multi-user, swarm + +### Serve leCore as a tool (/tools + /invoke) +run the HTTP service (holographic_service.serve) and any harness, LLM, or another leCore drives this node over two endpoints: GET /tools returns the manifest of every public faculty (name, description, params); POST /invoke with {name, args} runs one faculty and returns its result as JSON. Token-gated; private methods are refused. This is leCore AS a tool provider -- the same shape every node speaks.. + +```python +from holographic_service import serve; serve(host='127.0.0.1', port=8080, token='secret') # GET /tools ; POST /invoke {name,args} +``` +*Find it by:* serve as a tool, tool server, /tools, /invoke, expose faculties, http api, call leCore remotely, function calling + +## Data analysis & signals + +*analyse data and signals -- transport, graphs, embeddings, topology, FFT, faint-signal detection.* + +### Agreement across estimates (opponent) +given TWO estimates of the SAME thing (two models, two solvers, two forked worlds, two farm nodes), mind.opponent_channels(a, b) decomposes their disagreement (opponent-processing, ported from leOS) into: agreement (what both see), a_exclusive / b_exclusive (what only each sees), magnitude_dispute, PURPLE (a_exclusive + b_exclusive -- the emergent signal in NEITHER alone), and divergence_score (the angular disagreement). Act on the agreement when divergence is small; surface the conflict when it's large. classify() names the disagreement type; blend() mixes them by the channels.. + +```python +ch = mind.opponent_channels(est_a, est_b); if ch['divergence_score'] < 0.2: use ch['agreement'] # else look at ch['purple'] +``` +*Find it by:* opponent, agreement, disagreement, purple channel, consensus, vote, voting, ensemble + +### Data analysis +analyse data with VSA-native methods: optimal transport / Wasserstein (transport), graph Laplacian + spectral filtering (graphsignal), Nystrom embedding / dimensionality reduction, persistent-homology topology, kernel density estimate, point-cloud structure (cosmic), and time-series / market analysis. + +```python +from holographic_transport import wasserstein; from holographic_graphsignal import laplacian_filter +``` +*Find it by:* data analysis, cluster, optimal transport, wasserstein, graph laplacian, spectral, dimensionality reduction, embedding + +### Nystrom kernel (re-enable) +apply a kernel-weighted field in O(N*m) instead of exact O(N^2), gated by a low-rank probe: if a cheap held-out probe says the kernel is low-rank (smooth) use Nystrom (measured 6-14x faster, near-exact), else fall back to exact. The exact fallback is always correct, so the gate can't be wrong. + +```python +from holographic_nystrom import apply_kernel_gated; apply_kernel_gated(points, sources, weights, sigma) +``` +*Find it by:* nystrom, landmark, low rank, kernel, rbf field, large field, spectral embedding, o(n^2) + +### Scale (distribute) +make something bigger than one box / one pass can hold: partition a job, run the pieces independently, reassemble with a commutative monoid -- map_reduce, load-balanced partition, image tiles / volume bricks; strategies tiling/octree/multires/superposed/sparsefield. + +```python +from holographic_scalehome import Scale; Scale.map_reduce(buckets, worker, reduce='sum') +``` +*Find it by:* scale, distribute, partition, map reduce, tile, brick, parallel, shard + +### Signal & spectral +1-D signal processing: FFT / spectral analysis (spectral), faint-signal detection in noise with a calibrated false-discovery rate (signal_structure), drifting-narrowband / de-Doppler search (dedoppler), spectral flatness, and bandwidth. The radio-SETI-style detection stack. + +```python +from holographic_spectral import ...; from holographic_dedoppler import ... +``` +*Find it by:* signal processing, fft, spectral, spectrum, detect a signal, faint signal, narrowband, doppler + +### Symbolic reasoning +recover structure symbolically: symbolic regression to find a formula (symbolic), resonator networks that FACTOR a bound vector into its parts (sbc/resonator), is_a taxonomy climbing, and relational reasoning over records. Turning data and vectors back into laws. + +```python +from holographic_symbolic import ...; mind.climb('dog'); from holographic_sbc import ... +``` +*Find it by:* symbolic regression, find a formula, factor a vector, resonator, factorization, decompose a signal, reason, reasoning + +## Compression, codecs & video + +*shrink data losslessly or by rate-distortion, and handle temporal image sequences.* + +### Compression & codec +shrink data losslessly or by rate-distortion: a sequence/entropy codec (codec), general compression (compress), rate-distortion quantization (ratedistortion), and content-addressed storage (storage). How the engine fits vectors into bytes. + +```python +from holographic_codec import ...; from holographic_ratedistortion import ... +``` +*Find it by:* compress, compression, codec, entropy coding, rate distortion, quantize, content addressed storage, encode data + +### Video (temporal) +temporal image sequences: video compression with keyframe/delta coding (video), temporal compression, motion/phase morph between frames (phasemorph), and frame interpolation. Moving pictures on the substrate. + +```python +from holographic_video import ...; mind.blend_images(a, b) +``` +*Find it by:* video, compress a video, temporal compression, frames, motion, interpolate frames, keyframe, sequence of images + +## Honesty & measurement + +*measure claims honestly -- error bars, ablations, calibrated detection, proof-of-structure.* + +### Honesty & measurement +measure claims honestly: error bars + significance (measure), ablation studies (ablate), proof-of-structure against a null (structure), calibrated detection with false-discovery control, benchmark + variance harness, and stress tests. The engine's own truth-in-advertising tools. + +```python +from holographic_measure import ...; from holographic_ablate import ... +``` +*Find it by:* measure, error bars, significance, ablation, false discovery rate, calibrated, benchmark, variance + +## Navigation, planning & programs + +*find paths, plan routes, and run stored vector programs on the VSA machine.* + +### Graph traversal (exact) +reachability over a table's edges -- neighbors, descendants, reachable, shortest path -- what recursive SQL CTEs make painful. Uses an EXACT adjacency index by design: the holographic graph store's recall collapses at scale, so traversal is a plain deterministic graph (tombstone-aware, directed or undirected). + +```python +from holographic_querygraph import EdgeGraph; EdgeGraph(t,'src','dst').path(a,b) +``` +*Find it by:* graph, reachable, descendants, shortest path, traversal, adjacency, recursive cte, edges + +### Navigation & planning +find a way through a space or structure: A*/shortest-path route planning (plan), slime-mould flow networks (flow), tree/graph navigation (navigator), and maze solving. Pathfinding on the VSA substrate. + +```python +from holographic_plan import ...; mind.solve_maze(world); from holographic_flow import ... +``` +*Find it by:* navigation, plan a route, pathfinding, shortest path, maze, slime mould, flow network, route + +### Program & machine (VM) +the VSA computer: a stored-program holographic machine (machine/HoloMachine) that runs vector programs, recipes with holes / hygienic templates (template), a content-addressed compile cache (compile), tool-orchestration planning (orchestrator/voidsynth), and reversible computation. Programs as data. + +```python +from holographic_machine import HoloMachine; from holographic_template import RecipeTemplate +``` +*Find it by:* virtual machine, stored program, run a program, vm, recipe, template, recipe with holes, compile + +## Run it as a service / distributed + +*stand leCore up as an HTTP app, and scale work across a farm with jobs you can pause and resume.* + +### Command runner (external tools) +run any registered ALLOWLISTED program/script as a task (subprocess, no shell, time-boxed) and wire it as an orchestrator Tool the Planner can chain, with a CircuitBreaker on a flaky one -- the door to external tools and services. SECURITY: allowlist only, never a command from untrusted input, values fill placeholders. + +```python +from holographic_command import CommandRunner, command_as_tool; r.register('ffmpeg', [...]); r.run('ffmpeg', args) +``` +*Find it by:* run command, external tool, subprocess, shell, run program, ffmpeg, job runner, allowlist + +### Distributed coordinator +run monoid work (partition -> worker -> shared read-only cache -> reduce) on a pluggable BACKEND: an in-process default or a persistent local process pool (ProcessPoolExecutor + shared_memory, cache shipped ONCE, workers in separate interpreters). Sits behind distribute; includes a margin-gated canonical tie-break so distributed results agree on knife-edge decisions. + +```python +from holographic_coordinator import Coordinator, LocalPool; Coordinator(LocalPool(4)).run(buckets, worker, cache, reduce) +``` +*Find it by:* coordinator, distribute compute, process pool, parallel, render farm, offload, shared memory, backend + +### Distributed hardening (R5) +fault tolerance + verification for untrusted farm nodes: retry-with-backoff (a reissue reassigns a dead node's work), redundant computation + majority VOTING (accept only what independent nodes agree on -- a node can't force a result), canary buckets (known answers reject an untrusted node), and speculative straggler backups. The BOINC/SETI@home discipline, mandatory before public contributors. + +```python +from holographic_hardening import HardenedCoordinator; HardenedCoordinator(farm, redundancy=3).run(buckets, worker, cache, reduce, canaries=[...]) +``` +*Find it by:* voting, redundant compute, retry, fault tolerance, canary, untrusted node, quorum, straggler + +### Fork and apply a shared world (workspace) +mind.workspace.fork(name) hands out a copy-on-write editing view of a named world (a set of vector SLOTS): reads fall through to the shared base, writes accumulate in the fork's private .delta, so your edits don't touch the shared world (or another fork) until you reconcile. Feed the deltas to mind.merge_forks, then mind.apply(merged, world=name) writes the agreed edits back. Closes the fork -> edit -> merge -> apply loop; a world is a seed + deltas, so only the sparse changes travel.. + +```python +f = mind.workspace.fork('lab'); f.set('sky', v); mind.apply(mind.merge_forks([f.delta, other])['merged'], world='lab') +``` +*Find it by:* workspace, fork a world, apply changes, copy on write, world, shared world, checkout, branch a world + +### Job lifecycle control +start / pause / resume / cancel long-running work (renders, simulations, dataset processing) as CHECKPOINTABLE monoid jobs: completed buckets fold into partials, so a job pauses at a bucket boundary, saves to disk, survives an app restart, and resumes only the remaining buckets. Works across any coordinator backend (local pool / farm). + +```python +from holographic_jobs import JobManager; m.create(id, buckets, worker); m.start(id, background=True); m.pause(id); m.resume(id) +``` +*Find it by:* job, start, pause, resume, cancel, checkpoint, render job, long running + +### Messaging across machines (distributed bus) +the same publish/subscribe/send bus, spread across nodes: mind.distributed_bus(peers, token, node_id) publishes locally AND fans out to peer nodes (each running holographic_distbus.serve_bus), so agents on different machines share topics -- a swarm coordinates across the farm the way it does in one process. Received messages deliver local-only (no loops), dedup by a global id, and a dead peer never blocks the publisher. Bound a mailbox (open_mailbox(maxlen=)) for backpressure at high fan-out.. + +```python +bus = mind.distributed_bus(['hostB:9100'], token, node_id='A'); from holographic_distbus import serve_bus # serve_bus(bus, port=9100, token) in a thread +``` +*Find it by:* distributed bus, messaging across machines, cross-node messaging, swarm messaging, pub sub across nodes, fan out, gossip, backpressure + +### Query / database (domain) +treat VSA stores as a database: SQL over tables, similarity/time-travel/diff, durable + concurrent + graph + history query layers. + +```python +from holographic_query import run_sql, UserTable +``` +*Find it by:* query, sql, database, table, history, diff, time travel + +### Query time-travel & audit +git-for-data on a query table: SELECT as-of a past version (time travel), blame a row across versions, diff two versions (added/removed/changed with field detail), revert, branch/compare/discard, and prove/locate-tampering (Merkle root + O(log n) which-row-changed). Wires the shipped versioning faculties into the query layer. + +```python +from holographic_querytime import TableHistory, select_as_of, diff_versions, prove +``` +*Find it by:* time travel, as of, temporal, blame, diff versions, revert, branch, git for data + +### Single-writer concurrency +B8 concurrency: one writer at a time (serialised by an exclusive lock; a second writer waits or fails fast) plus lock-free reader SNAPSHOTS (a consistent point-in-time view immune to later writes). MVCC deferred, stated honestly. + +```python +from holographic_querylock import SingleWriterLock; with lock.write(): ... +``` +*Find it by:* lock, single writer, concurrency, snapshot read, writer lock, isolation, consistent read + +### Standalone API service +run the engine as a standalone DATABASE server on any OS and talk to it over HTTP/JSON: full SQL (CREATE/INSERT/SELECT/UPDATE/DELETE/JOIN/DROP), a GraphQL front door for nested documents, disk PERSISTENCE (data survives a restart), capability discovery, and an optional bearer-token gate. Stdlib-only (numpy aside); a drop-in DB replacement for other apps. Launched by serve.sh (Linux/macOS) / serve.bat (Windows). + +```python +./serve.sh --persist mydb.json # then: curl -X POST .../sql -d '{"sql":"SELECT ..."}' +``` +*Find it by:* api, server, service, standalone, http, rest, daemon, database + +### VSA programs as DB objects +installable, runnable 'stored procedures' that are hypervectors the machine executes (LOAD/BIND/APPLY/HALT -- not arbitrary code): install, list a queryable catalog, find a program BY MEANING (fuzzy over its doc), EXPLAIN (dry run), and EXECUTE over query rows sandboxed to whitelisted handlers + step-bounded, result carrying a calibrated confidence. Safer than a SQL stored procedure. + +```python +from holographic_queryprog import ProgramCatalog; cat.install(...); cat.find('cluster a series') +``` +*Find it by:* stored procedure, install program, execute program, udf, pg_proc, find program, run program, vsa program + +### Workspace folders +a shallow grouping tree over a database's tables (database > folder > table): each table has one HOME folder (ownership -> lifecycle/tier) plus any number of ASSOCIATION links (grouping, no deletion on unlink). Scoped search runs over just a subtree. Folders reference existing tables, they do not copy them. + +```python +from holographic_queryfolder import FolderTree; ft.set_home('user.sales','reports'); ft.tables_in('reports') +``` +*Find it by:* folder, group tables, namespace tree, organize tables, home folder, association folder, scoped search, drill down + +### Workspaces (durable DB + transient sessions) +WS3-WS6: run one persistent user database alongside many TRANSIENT per-session workspaces (loose scratch tables + the 3D/sim/render context) that stay isolated -- clearing or resetting one never touches the persistent DB or a sibling. Make / switch / clear / reset-keeping-data, export/import a workspace, and combine two with an EXPLICIT collision policy (a merge is a decision, not a guess). + +```python +from holographic_workspace import WorkspaceManager; m=WorkspaceManager(); m.new_workspace('sessionA'); m.switch_workspace('sessionA') +``` +*Find it by:* workspace, session, scratch tables, transient tables, isolate session, reset keep data, export workspace, combine workspaces + +## More capabilities + +### Blend (combine) +combine things into one: bundle (superposition, weighted = soft mixture), lerp / slerp interpolation, Frechet mean on the sphere, front-to-back alpha composite, and dict/scene merge with a conflict policy. + +```python +from holographic_blendhome import Blend; Blend.bundle(vectors, weights) +``` + +### Coarse-first refine (re-enable) +run the cheap method everywhere, measure a per-cell residual/uncertainty, and escalate to the expensive method ONLY where it's high -- the shared detector for the Group-B re-enables (adaptive AA, Nystrom, splat refine, volint). concentration() is the honest breakeven check (low => uniform is just as good). + +```python +from holographic_coarsefirst import refine_where_uncertain, concentration +``` + +### Denoise (domain) +clean a render or signal with one home: image SVGF (variance-guided a-trous) or demodulated (divide albedo out), sharpen, and the signal manifold denoisers (adaptive/manifold/codebook/trajectory). + +```python +from holographic_denoisehome import Denoise; Denoise.image(img, N, A, D, method='svgf') +``` + +### Durability & crash recovery +B7: make the query store survive a crash. Take a durable SNAPSHOT of the persistent tiers (replay-based, so it rebuilds byte-identically), keep a write-ahead JOURNAL of inserts/updates/deletes since the snapshot, and RECOVER to the last consistent point by loading the snapshot and replaying the journal. The snapshot+WAL discipline, on top of the plain save/load the service already exposes. + +```python +from holographic_query_durable import save_snapshot, Journal, recover; recover(snap_path, journal_path) +``` + +### Invite guests and share selectively (access control) +control who reads what. mind.invite(kind, grants) mints a token admitting a guest with specific initial read grants; mind.admit(code, id) redeems it into a scoped Principal that reads ONLY what it was granted (default: nothing but its own namespace) and writes only its own. mind.grant / mind.revoke share and un-share namespaces later; holographic_access.require_readable is the read chokepoint (the symmetric twin of the DB's write-only-your-own rule).. + +```python +code = mind.invite(kind='user', grants={'read':['lab/scene']}); g = mind.admit(code, 'visitor'); mind.grant(g, read='lab/notes') +``` + +### Merge forked worlds (fork/merge) +mind.merge_forks(forks, policy, tol) reconciles several forked copies of a world, each a {slot: vector} delta. Slots the forks AGREE on merge conflict-free into the consensus (pairwise opponent divergence below tol, matching leOS's pairwise convention); slots they DISAGREE on are handled by policy: 'select' surfaces the conflict for a human, 'auto' keeps only agreements, 'left'/'right'/callable resolve it. Because a world is a seed + deltas, forking to single-player and merging back is cheap. Returns {merged, conflicts}.. + +```python +res = mind.merge_forks([mine, theirs], policy='select'); apply(res['merged']); resolve(res['conflicts']) +``` + +### Refine loop (produce / critique / adjust) +mind.refine(produce, critique, adjust, accept, budget) makes a result, has a CRITIC score it (a metric, opponent agreement, a model, or a human), adjusts, and retries until it's good enough or the budget runs out -- the pipeline middle that sits leCore between a big compute and a checker. Returns {result, score, accepted, tries}. The callable-critic sibling of project_onto_constraints.. + +```python +log = mind.refine(produce=lambda: gen(), critique=score, adjust=lambda r,s: tweak(r,s), accept=0.9) +``` + +### Regime gate (re-enable) +run a superior-but-niche method ONLY in its regime, behind a cheap conservative detector, with a safe fallback everywhere else -- the pattern for re-enabling a shelved 'kept negative' now that adaptive dispatch can spot its regime (e.g. closed-form iterate for linear/bind operators). + +```python +from holographic_regimegate import RegimeGate; RegimeGate(name, detect, threshold, superior, fallback) +``` + +### Sampling +Monte-Carlo sampling: low-discrepancy / blue-noise patterns, cosine-hemisphere directions, MIS weighting, firefly-clamped accumulation -- one home over the shipped samplers. + +```python +from holographic_samplinghome import Sampling; Sampling.cosine_hemisphere(N, n, seed) +``` + +### Use external tools (remote nodes / LLMs / commands) +leCore CALLS tools in the same shape it serves them. holographic_toolclient.remote_tools(base_url, token) fetches another node's /tools and yields each as a callable RemoteTool (its run(args) POSTs to that node's /invoke). mind.attach_llm(callable) wires an LLM (any text->text, no SDK). mind.orchestrator.register / register_command / register_remote add remote tools, shell programs (allowlisted), and whole remote nodes so a planner can chain local faculties, remote tools, LLMs, and commands uniformly.. + +```python +for t in remote_tools('http://host:8080', token='x'): mind.orchestrator.register(t) # + mind.attach_llm(llm); mind.orchestrator.register_command('ffmpeg', ['ffmpeg','-i','{}']) +``` + +### Utilities & helpers +the engine's cross-cutting UTILITY tools: content addressing & hashing (uri), tamper-evident verification (verify), erasure/rateless coding for reliability (fountain), chunked delta chains with integrity proofs (deltachain), versioned rollback history (history), lossless compression (compress/codec), and the determinism contract (determinism). The plumbing every faculty leans on. + +```python +from holographic_uri import address_from_content, make_key; from holographic_verify import CompositionTree +``` + +--- + +*87 capability homes. Regenerate this file with `python capdoc.py` (it reads the live catalog, so it stays in step with the engine).* diff --git a/CONVENTIONS.md b/CONVENTIONS.md new file mode 100644 index 00000000..29e9b8d4 --- /dev/null +++ b/CONVENTIONS.md @@ -0,0 +1,74 @@ +# Conventions & Gotchas + +*The handful of conventions that are load-bearing and easy to get wrong. Each one below cost real time on a +demo when it was gotten wrong the first time. Read this page before starting a new demo or app, so you meet +these once here instead of one bug at a time. (This is the "conventions & gotchas" section for the dev guide; +it lives as its own file so it is easy to find and to paste into the guide.)* + +--- + +## 1. SDF sign — negative inside + +A signed-distance function returns the distance to the nearest surface, **signed**: + +- **negative inside** the surface, +- **zero** on it, +- **positive outside**. + +Every SDF in the engine follows this, and `SDFScene.eval` returns the `min` over parts — so the scene's +zero-level-set is the union of its parts. If you write a primitive with the sign flipped, the ray-marcher walks +the wrong way and the surface turns inside-out. When in doubt, evaluate your SDF at a point you know is inside +and check it is negative. + +## 2. Colour space — shade in linear, tonemap + gamma at the very end + +The renderer hands back a **raw linear** radiance buffer. It is *not* display-ready — showing it directly looks +washed out or blown out. The display pipeline is, in order: + +1. render → linear radiance (HDR, can exceed 1.0), +2. **exposure** (scale linear radiance; done in HDR, *before* tonemapping), +3. **tonemap** (e.g. Reinhard `x/(1+x)` or ACES — compress HDR → LDR), +4. **gamma / sRGB** encode (the last step, linear → display). + +The gotcha: **mixing display-space and linear values silently washes things out.** Keep everything linear until +the tonemap+gamma at the very end (that is what `holographic_postfx` does). Don't gamma-encode twice, and don't +feed an already-tonemapped image back into a linear operation. + +## 3. Camera handedness — `right = forward × up`, `view-up = right × forward` + +The camera looks from `eye` toward `target`: + +``` +forward = normalize(target - eye) +right = forward × up # (world up) +view-up = right × forward +``` + +`holographic_camera` and `holographic_transform.look_at` both follow this (the camera looks down **−z** in view +space, y is up — the OpenGL convention). The gotcha: **a mirrored `right` vector renders the whole scene +flipped left-to-right**, and it is easy to do by writing `up × forward` instead of `forward × up`. If your scene +comes out mirrored, this cross-product order is the first thing to check. + +## 4. Determinism — the trio that keeps outputs reproducible + +Non-negotiable, engine-wide. Every output must be reproducible bit-for-bit across runs: + +- **`PYTHONHASHSEED=0`** — set it in the environment; otherwise set/dict iteration order can vary run to run. +- **`hashlib`, never Python's `hash()`** — `hash()` is salted per-process and is *not* stable; use `hashlib` + (e.g. sha256) for any content hash or content-addressed key. +- **stable sorts** — sort by an explicit key that fully determines order (add a tie-break like the index), so a + tie never resolves differently on another run or platform. + +The deeper lesson behind the trio (the `bind_batch` story): **a change that is bit-identical to 1e-12 can still +flip a downstream discrete decision** (it flipped a creature's maze trajectory once). So on any tie-sensitive +path — argmin, argmax, a sort tie, a threshold — preserve the *exact* arithmetic and ordering, and keep new +"equivalent" fast paths out of it unless they are bit-for-bit identical. + +--- + +## Bonus gotcha — import-time `NameError` + +A module-level constant that references a name defined **lower** in the same file raises a `NameError` the +moment the module is imported (before any function runs). This bit the garage demo repeatedly. It shows up +instantly as an import failure — which is exactly why `tools/demo_kit.smoke_test` catches it: it *imports* the +backend as its first step, so a use-before-def surfaces immediately rather than at request time. diff --git a/FEATURE_GUIDE.md b/FEATURE_GUIDE.md new file mode 100644 index 00000000..dac3d9cb --- /dev/null +++ b/FEATURE_GUIDE.md @@ -0,0 +1,380 @@ +# leCore Feature Guide + +A hands-on guide to the features added most recently — composable materials and textures, the describe-a-scene +authoring flow (naming, texturing, external files), external-asset relocation and the queryable file map, the +message-bus + optional-agent harness, the opt-in language layer (dictionary + semantic word search), and cold storage +for compressing inactive data. + +Everything here is reached through one object, the `UnifiedMind`: + +```python +from holographic_unified import UnifiedMind +mind = UnifiedMind(dim=1024, seed=0) # dim = hypervector width; seed keeps everything deterministic +``` + +Three ground rules worth knowing up front: + +- **Deterministic.** Same inputs + same seed → same output, every run (ids come from `hashlib`, never Python's `hash()`). +- **Opt-in / lazy.** The heavy parts (the dictionary, a semantic index, an image decoder) load only when you actually + use them, so importing the library to build on top of it costs you nothing. +- **Honest.** Where a feature is approximate or has a known limit, this guide says so plainly under *"Kept limits"*. + +If you ever forget which call does what, ask the engine in plain English: + +```python +for home in mind.find_capability("paint a texture onto an object and render it"): + print(home.name) +``` + +--- + +## 1. Composable materials and textures + +A texture is built as a small **graph of operations** over typed inputs — leaves (procedural sources or constant +colours) feed operators (`mix`, `multiply`, `scale`, `over`, `saturate`, …) that feed other operators. The whole graph +is type-checked when you compose it, not when you render. + +```python +# leaves: a procedural source, or a constant colour (colour NAMES work too) +noise = mind.texture_leaf("fbm", n_dims=2, seed=0) # a fractal-noise field +orange = mind.texture_leaf(value="orange") +purple = mind.texture_leaf(value="purple") + +# an OP node blends the two colours by the noise field. NOTE the name: texture_OP, not texture_map -- +# texture_map is the older image-based texture; texture_op builds a procedural graph node. +tex = mind.texture_op("mix", a=orange, b=purple, t=noise) + +rgb = mind.sample_texture(tex, [0.3, 0.7]) # sample the graph at a UV -> an rgb value +``` + +Four "costumes" wrap the same composition machinery for common jobs: + +```python +# CMP3 -- blend whole MATERIALS by per-point masks (weights become a partition of unity) +blend = mind.multi_material([mat_a, mat_b], weights=[w_mask, 1.0], mode="mask") + +# CMP2 -- LAYER materials in a fixed order (base < diffuse < specular/reflection < coat); the order is +# schema-checked at compose time, so you can't stack a base coat on top of a clear coat by mistake +layered = mind.layered_material([base_layer, coat_layer]) + +# CMP4 -- INSTANCE shared geometry: define once, place many; edit the definition and every instance updates +scene = mind.instanced_scene(definitions, instances) + +# CMP5 -- a RENDER GRAPH that decides what to bake once vs sample live, then prepares a scene to render +rg = mind.render_graph() +``` + +**Preview** a texture or material without a full render: + +```python +swatch = mind.preview_texture(tex) # a flat RGB thumbnail of the texture +matball = mind.preview_material(material) # a Cook-Torrance-shaded sphere of the material +``` + +**Kept limits.** A texture's `sample` is a cosine read-out of a vector field — it's direction/scale-normalised, so tune +by ratios, not absolute values. Layer *ordering* is a correctness rule, not energy-conserving radiometry. + +--- + +## 2. Describe a scene, name it, texture it + +Describe a scene in plain words; adjust it by talking to it. + +```python +scene = mind.build_scene("a big red metal sphere and a small blue box on a sunny day") +scene.adjust("make the sphere bigger") +scene.adjust("change the box to metal") +img = scene.render(width=320, height=240) # an (H, W, 3) image in [0,1] +``` + +**Name objects** so you can refer to them easily. A nickname always wins over description-matching, so once you name +something you can always reach it: + +```python +scene.name("the red sphere", "hero") # give it a nickname +scene.adjust("make hero glass") # reference it by that nickname +scene.adjust("rename hero to champion") # rename in plain English +scene.adjust("call the box crate") # name a second object +print(scene.labels()) # {'champion': 'red big glass sphere', 'crate': 'blue small box'} +``` + +**Texture objects** with built-in procedural textures — by talking to the scene, or via the API: + +```python +scene.adjust("give champion a rusty texture") # rusty / marbled / mossy / cloudy / lava / striped / noisy +scene.adjust("make the box mossy") +scene.paint("crate", "marbled") # the same thing through the API +img = scene.render() # render() automatically paints attached textures on +``` + +**Attach an EXTERNAL image file** as a texture (see §3 for what happens when those files move): + +```python +scene.attach_texture_file("the sphere", "project/textures/wave.png") +img = scene.render() # loads the file's pixels and paints them on +``` + +**Kept limits.** UV mapping is the textbook kind (a seam + pole pinch on a sphere, face seams on a box); the fast render +uses a single hard light — reach for the path tracer (`render(quality="hyperreal")`) for soft shadows / GI. External +image decoding uses PIL, imported *lazily* only when you actually draw an image file, so the core stays NumPy-only. + +--- + +## 3. External assets and the queryable file map + +### 3a. Track external files and repair paths when they move + +Real pipelines move folders around and break every reference at once. An `AssetLibrary` fixes them the way you'd reason +about it: re-point **one** file, and it works out the parent that moved and re-finds the rest. + +```python +lib = mind.asset_library() +lib.add("/proj/textures/water/wave.png") +lib.add("/proj/textures/stone/wall.png") +lib.add("/proj/models/boat.obj") + +# ... the whole /proj folder was moved to /work/proj ... +print(len(lib.missing())) # 3 -- all broken + +# re-point ONE; the other two are found automatically (shared moved-parent + a structural search) +report = lib.relink(lib.assets[0], "/work/proj/textures/water/wave.png") +print(len(lib.missing())) # 0 +``` + +Know when an external file was **edited on disk**: + +```python +for ref in lib.changed(): # size/mtime (cheap) or content hash (definitive) + print("re-import:", ref.path) + ref.refresh() # acknowledge it +``` + +**Distributed / cross-machine.** Absolute paths differ per machine, so identify files by **content hash** and resolve +them wherever they landed: + +```python +lib.add_hashes() # record a content hash per file (do this once) +path = lib.resolve(some_ref, roots=["/mnt/shared/assets"]) # finds it by content, not by path +lib.save("assets.json") # a portable JSON manifest +``` + +A `SemanticScene` carries its own library, so external textures self-heal at render time: + +```python +scene.set_asset_roots(["/work/proj"]) # where to search if files moved +scene.resolve_assets() # re-find any missing files +img = scene.render() # missing files fall back to the object's colour -- never crashes +``` + +**Kept limits.** The path logic is POSIX-tested; Windows drive letters are handled simply, not exhaustively. + +### 3b. Digest a folder or zip into a queryable file map + +Point at a folder, a `.zip`, or a single file and get back a `FileMap` you can query five ways: + +```python +fm = mind.ingest_files("my_project.zip") # a folder, a .zip, or one file + +fm.find("*.png") # by NAME / glob +fm.by_kind("model") # by KIND: image / text / model / data / code / archive / other +fm.larger_than(1_000_000) # by METADATA (also newer_than, by_ext) +fm.search_text("normal caustic") # by text CONTENT (an inverted index over the text/code files) +fm.tree() # the folder hierarchy as nested dicts -- the "file map" + +# by MEANING (opt-in: builds a small vector index over the text, then searches by description) +fm.build_meaning_index() +fm.find_by_meaning("lighting setup") +``` + +Every ingested file is tracked in a built-in `AssetLibrary`, so the same map self-heals: `fm.missing()`, `fm.changed()`, +`fm.relink(one, new)`, `fm.resolve_assets(roots)`. + +**Kept limits.** Text indexing reads only text/code kinds under a size cap (so a pile of big binaries stays cheap). +Meaning search is approximate random-indexing — reliable for the top hits, noisy in the tail. + +--- + +## 4. The agent harness: a message bus + an optional LLM + +leCore is the core of an AI-substrate harness: a person and an agent can both be attached to the running tool, and the +app **pushes** to the agent instead of the agent polling. It's built on a small message bus. + +```python +bus = mind.bus() # a topic-based message bus (publish / subscribe / mailboxes / history) +bus.subscribe("render.*", lambda m: print("event:", m.topic)) +bus.publish("render.start", {"w": 320}) +``` + +Connect an **optional** agent — any callable `text -> reply` (your wrapper around any model; no LLM library is imported, +so this is entirely optional and the app runs fine with no agent attached): + +```python +bridge = mind.agent_bridge(llm=my_llm_function) # llm=None also works -- events just get logged +bridge.notify_on("render.done", "Does this look right?") # PUSH to the agent when a render finishes +bridge.on_reply(lambda m: print("agent said:", m.payload["reply"])) +answer = bridge.ask("what can you do?") # ask it directly +``` + +Run any job as a **task** that announces itself — this is the "check after the render is done" pattern, with no polling: + +```python +# runs the render in the background and publishes 'render.done' (with a small summary) when it finishes; +# the bridge above then calls the LLM automatically +mind.run_task("render", + lambda: scene.render(width=640, height=480), + background=True, + summarize=lambda img: {"shape": list(img.shape)}) # an LLM can't read a NumPy image, so hand it a summary +``` + +Over HTTP, a remote agent uses `POST /bus/publish` and `POST /bus/poll` (its inbox) — see `SERVICE.md`. + +**Kept limits.** The push side is in-process (a callback) or a pulled HTTP inbox; there is no live server-push (SSE/ +websocket) yet. + +--- + +## 5. The language layer: dictionary + semantic word search + +A ~144k-word English dictionary (Princeton WordNet) gives the engine real-world grounding. It is **opt-in and lazy** — +it never loads from importing leCore or building a mind, only from the first language call, then it lives in RAM as a +plain dict (fast lookups). + +```python +mind.lookup("gravity") # {'definition': ..., 'pos': ..., 'synonyms': [...], ...} +mind.word_taxonomy("dog") # 'a dog is a kind of ...' + +import holographic_dictionary as hd +hd.stats() # {'loaded': False, 'source': 'dictionary.json.xz', ...} -- reading this does NOT load it +hd.preload() # force the one-time load at startup (optional) +hd.unload() # drop the ~22 MB back; the next lookup transparently reloads +``` + +Search the dictionary by **meaning** (the fuzzy reverse of a lookup) — opt-in, since it builds a vector index: + +```python +idx = mind.build_semantic_index(words=my_vocab) # or words=None for the whole dictionary (~150 MB at dim=256) +idx.find("unexpected good luck") # -> 'serendipity' +idx.similar("puppy") # -> 'dog', 'kitten' +``` + +**Kept limits.** The dictionary itself is exact. The semantic index is approximate random-indexing over one gloss per +word — great for the top hit, noisy in the tail, and word-sense sensitive (it only sees the single stored sense). + +--- + +## 6. Cold storage: compress inactive data, inflate on demand + +A long-running app holds a lot of *idle* data — tables nobody has queried lately, a database belonging to another +session, a cache you built once. Cold storage folds those up (serialize + compress, freeing the live object) and +unfolds them transparently the next time something touches them. Nothing is lost; it's the same object, just compressed +while it wasn't needed. + +Wrap **one** value: + +```python +c = mind.cool(big_table) # or codec="lzma" for a smaller blob, spill_dir="/tmp/cold" to write it to disk +c.cool() # serialize + compress + free the live object's RAM +big_table = c.get() # bit-identical, inflated on access +print(c.ratio()) # cold / warm size -- smaller is better +``` + +Or bound memory across **many** values with an auto-cooling store — it keeps only the K most-recently-used live and +compresses the rest, warming any of them the instant you `get()` it: + +```python +store = mind.cold_store(keep_warm=8) # at most 8 stay warm +for name, table in my_tables.items(): + store.put(name, table) +t = store.get("orders") # if it was cold, it's transparently warmed here +print(store.stats()) # {'warm': 8, 'cold': N, 'cold_bytes': ..., 'approx_saved_bytes': ...} +``` + +It works on anything picklable — a `Table`, a whole `Database`, a big NumPy array, an ordinary dict. `spill_dir=...` +writes cold blobs to a file so even the compressed bytes leave RAM. + +**The query Database can auto-cool its own idle tables.** Turn it on (it's off by default), and tables you haven't +queried lately compress; the next query warms them back transparently: + +```python +db.enable_cold_storage(keep_warm=8) # keep the 8 most-recently-used tables warm +db.cool_idle() # compress the rest (call this when the DB is idle -- no query in flight) +db.resolve("app.orders") # a query warms a cold table automatically +db.cold_stats() # {'warm': ..., 'cold': ..., 'cold_bytes': ..., 'enabled': True} +``` + +This is **safe in distributed compute**: if a cold-enabled database is shipped to a worker (used as a shared read-only +cache), it arrives *warm with cooling disabled* — a plain, immutable copy — so a worker's reads never mutate the shared +cache, and the lock and any spill-file paths never cross the process boundary. Cool on the long-lived main node to save +memory; workers get safe warm copies. (Cool only when idle: cooling swaps a table for its compressed form, and warming +later builds a fresh object, so doing it mid-transaction could strand a live reference.) + +**Kept limits.** How much you save depends entirely on the data. Redundant / text / structured data compresses a lot +(a repetitive array can drop to ~0.1% of its size). But leCore's **VSA record vectors are near-random (high-entropy), +so they barely compress** — there the real win is freeing the live Python object and (optionally) spilling the blob to +disk, not the compression ratio. And because it uses `pickle`, only cool data your own app produced — never thaw a blob +from an untrusted source. + +--- + +## 7. Importing artist file formats + +Bring in the files artists actually hand you. One dispatcher, `mind.import_asset(path)`, picks by extension; or call the +specific loader. + +```python +# Wavefront OBJ (+ its .mtl): positions, per-corner UVs/normals, the material each face uses, and the materials +# themselves (Kd/Pr/Pm factors + map_* textures loaded) +lm = mind.load_obj("chair.obj") +lm.positions # (Nv, 3) +lm.faces # (Nf, 3) triangles (polygons are fan-triangulated) +lm.materials # {name: PBRMaterial} +mesh = lm.mesh() # a plain engine Mesh for the geometry pipeline + +# glTF / GLB: geometry AND its PBR materials (base colour / metallic-roughness / normal / occlusion / emissive), +# per-vertex UVs and normals, embedded textures, and -- for rigged models -- animation and skinning +glb = mind.load_glb("robot.glb") +mat = list(glb.materials.values())[0] # .base_color, .metallic, .roughness, .base_color_map, .normal_map, .ao_map... +glb.uv # per-vertex UVs (TEXCOORD_0), or None +glb.normals # per-vertex normals, or None + +# rigged/animated glTF: keyframed node transforms + the skeleton +for clip in glb.animations: # each is an AnimationClip + print(clip.name, clip.duration) # e.g. "Walk 1.20" + pose = clip.sample(0.5) # {node_index: 4x4 local matrix} at t=0.5s (rotations SLERPed) +glb.skins # [{'joints': [...], 'inverse_bind': (J,4,4)}] -- the skeleton +glb.joints, glb.weights # per-vertex skin binding (JOINTS_0 / WEIGHTS_0), or None + +# DEFORM the rig -- make it actually move. Morph-blends the base shape (if it has blend shapes) then applies +# linear-blend skinning by the posed skeleton, returning the deformed mesh at time t. +posed = mind.deform_mesh(glb, clip=glb.animations[0], t=0.5) # a Mesh with vertices moved to the pose at t=0.5s +rest = mind.deform_mesh(glb, clip=None) # the rest pose (no animation) + +# A folder of maps exported from Adobe Substance 3D Painter (or any tool) -> one PBRMaterial. Maps are matched by +# file name: basecolor / roughness / metallic / normal / height / ao / emissive. +brick = mind.load_texture_set("exports/brick") +brick.channels_found # e.g. ['ao', 'base_color', 'height', 'metallic', 'normal', 'roughness'] + +# A volumetric density grid -> a field the volume renderer marches +field, bounds = mind.load_volume("smoke.npy") # or raw floats: load_volume("d.raw", dims=(nx,ny,nz)) +img, alpha = mind.render_volume(field, camera, bounds, mode="smoke") +``` + +**Kept limits (stated plainly).** We import the *open, exported* forms. The proprietary project files need their +vendor's engine and are **not** parsed: Substance's `.sbsar` / `.spp` (export the texture maps from Painter instead), +and OpenVDB's sparse `.vdb` (export a dense `.npy`/`.raw` grid, or convert with the OpenVDB tools — `load_volume` +refuses a `.vdb` rather than guessing). Image decoding uses PIL, imported lazily only when a texture is actually +loaded, so the core stays NumPy-only; a texture that can't be found becomes `None` and the factor-level material still +works. OBJ handling covers the common case (v/vt/vn/f/usemtl/mtllib, fan-triangulated polygons); exotic OBJ features +are ignored, not errored. The deformer applies **linear-blend** skinning (the standard method; it has the classic +candy-wrapper collapse at extreme twists that dual-quaternion skinning avoids — not implemented) and blends morph +targets; it uses the first skin and moves positions (normals aren't re-skinned). OBJ carries no animation. + +--- + +## Where to look next + +- **`mind.find_capability("...")`** — ask the engine, in plain English, which call does what. +- **`CAPABILITIES.md`** — the full menu of capability "homes" (auto-generated from the catalog). +- **`API_QUICKREF.md`** — one scannable line per public function (auto-generated). +- **`SERVICE.md`** — the HTTP endpoints, including the message bus. +- **`tour.py`** — a runnable tour that exercises these features end to end and prints what each one did. diff --git a/GALLERY.md b/GALLERY.md new file mode 100644 index 00000000..df3d52e0 --- /dev/null +++ b/GALLERY.md @@ -0,0 +1,227 @@ +# leCore — Gallery: visual output & measured behaviour + +*A showcase of what the engine actually produces. The 3-D renders, procedural patterns, reaction–diffusion +frame, and the four data charts are **generated fresh from the engine** by `make_gallery.py`; the rest come from +the committed test/benchmark harness (`figures/`). Everything is pure NumPy — no GPU, no pretrained models. The +performance/behaviour numbers are from single-thread sandbox runs, so treat them as ballpark, not spec.* + +*(Visual companion to [`REFERENCE.md`](REFERENCE.md), which maps the code.)* + +--- + +## Rendering & 3-D + +A from-scratch Monte-Carlo path tracer on signed-distance geometry — diffuse, metal, and dielectric materials, +lit by a **high-dynamic-range sun-and-sky environment** (a bright sun disk for real highlights, so metal and +glass have something with contrast to reflect and refract) and tone-mapped with the **ACES filmic curve plus +auto-exposure** (each scene metered onto mid-grey), rather than the flat Reinhard map that greyed everything out. +Every scene below is rendered by **one auto-calibrating call** +(`holographic_gbuffer.render_auto`) with a single `quality` knob and **no per-scene tuning**. It samples in +passes and, after each pass, asks the calibrated stop rule (`holographic_adaptive_sample.converged_mask`) which +pixels have reached the target confidence interval — those stop, the rest keep sampling — then denoises with a +**variance-guided SVGF** filter whose per-pixel strength comes from the noise the sampler measured. So samples +and blur land exactly where each scene needs them: the flat sky converges in one pass, while glass edges and +grazing reflections quietly pull several times more samples. + +![Path-traced spheres on a checker floor](gallery/render_spheres.png) + +*Three spheres — diffuse red, gold metal (GGX), blue — on a checker floor. Rendered by `render_auto` at +`quality="high"`; the adaptive sampler spent ~31 mean / 64 max samples per pixel across 8 passes, concentrating +on the metal and the checker reflections.* + +![A glass sphere refracting the scene behind it](gallery/render_glass.png) + +*A clear **glass** sphere in front of coloured spheres. The material returns an index of refraction (IOR 1.5), so +the tracer bends rays through it; **chromatic dispersion** splits white light into a coloured fringe (red, green +and blue traced with slightly different IOR — blue bends most, the Cauchy relation); and a real **caustic** — +light forward-traced *through* the sphere and splatted where it focuses (`holographic_globalillum.caustics`) — +brightens the floor beneath it. A forward path tracer can't find caustic paths inline, so the focused light is +brought to the floor separately, then composited before tone-mapping.* + +![A Menger-sponge fractal](gallery/render_fractal.png) + +*A **Menger sponge** (3 recursion levels) ray-marched from its signed-distance function. The geometry is +*generated*, not stored — the SDF is a few bytes whether it resolves to 100 k or 250 k faces.* + +![One surface, three identities](gallery/render_identities.png) + +*One surface, **three identities**. The *same* rounded shape is rendered three times as matte clay, polished +copper, and clear glass — and each is a **physical material pulled from the library** (`holographic_matlib`), so +the renderer reads the metallic / roughness / IOR straight off the material rather than from hand-typed numbers. +The engine carries a surface as one field, and a "material" is just a different physical read of it.* + +![A groomed furry critter](gallery/render_fur.png) + +*Fur, shaded by a **physical fiber material** (`holographic_matlib`'s `fur_ginger` → a Marschner strand BSDF: the +colour drives absorption, the roughness/cuticle-tilt set the R/TT/TRT lobes). Groom grows strands straight out +along the surface normal, so the coat is then **combed** — each strand bent from its normal toward a flow +direction so it lies along the body and flows, instead of standing on end. Rendered at 2× and box-downsampled +(**supersampled anti-aliasing**, since strands rasterise as 1-px lines), and lit with a **key** light (reveals +the groomed form) plus a softer warm **rim** from behind (the translucent fur edges glow). A normal-based surface +shader can't do this — hair scatters around its tangent, not a surface normal.* + +![An ocean in a box with a floating cube](gallery/render_ocean.png) + +*An **ocean in a box, with buoyancy**, rendered with a dedicated **water shader** (an open water surface over a +floor doesn't fit the path tracer's closed-object glass model — which is why the earlier version went black). +Where a camera ray meets the rippled surface, **Fresnel** splits it into a reflection of the sky and a +**refraction** down into the water; the refracted ray is marched to the sandy floor and faded by **Beer-Lambert +absorption** over the underwater distance — water absorbs red first, so deep water reads blue and dark (the +volumetric depth). **Pool caustics** (`holographic_globalillum.caustics`) brighten the sand, and a wooden cube +floats at its **Archimedes waterline** (submerged fraction = density ratio ≈ 0.6).* + +![The full light rig: spot with a gobo, rect area light, and IES downlight](gallery/render_light_types.png) + +*__The full light rig__ — the complete set of placed lights a real DCC app has, all sampled by next-event estimation so all cast correct shadows. Three pillars and a sphere against a backdrop, lit by: a __spot with a gobo__ (a striped light cookie projected across its cone, left, warm), a soft __rect area light__ (a softbox, middle), and an __IES downlight__ (a real luminaire's measured beam shape, right, cool). Beyond these there are also point, directional, ambient, sphere, and mesh (emissive-geometry) lights, and every light's colour or intensity can be a __field__ that varies across the scene. `load_ies()` reads a real .ies photometric file.* + +![A scene lit by placed lamps with soft shadows](gallery/render_lit_scene.png) + +*__Placed lights + next-event estimation__ — a scene lit by real lamps you put in the world, with correct shadows, instead of only a big sky. The path tracer used to gather light only when a bounce ray happened to escape and hit the emissive environment, so a small bright lamp was almost never found — hopeless noise. Next-event estimation points a shadow ray __straight at each light__ and adds its contribution directly, so lamps converge instantly. Here a warm sphere light (its area gives __soft__ shadows) and a cool point light from the other side light three objects in a dark room. The random bounce still runs, so indirect light — colour bleeding, ambient fill — isn't lost.* + +![Thin-film iridescence: a soap bubble and an oil-slick sphere](gallery/render_iridescence.png) + +*__Thin-film iridescence__ — the rainbow sheen of a soap bubble or oil slick, from real interference physics. A thin transparent film reflects light off both its top and bottom surfaces; the two beams interfere, and whether a colour reinforces or cancels depends on the film thickness __and the view angle__ — so the hue sweeps across a curved surface and shifts as it tilts. Left: a soap bubble (~300 nm film). Right: an oil-slick sphere (~440 nm). The colour is computed from two-beam interference and integrated against the eye's CIE response (reused from the blackbody code), then the path tracer tints the reflection by angle. It comes from the material's film thickness, not a painted texture.* + +![Crystal grains and ore inclusions](gallery/render_crystal.png) + +*__Physical-structure materials__ — the colour comes from the material's internal __structure__, sampled at each point, not a flat swatch. Left: a __polycrystalline gem__ — a Voronoi grain partition where every facet is a slightly different colour, darkened along the grain boundaries. Right: an __ore boulder__ — a base rock shot through with impurity __inclusions__ (metallic pockets at a calibrated coverage, the planet's ore-deposit pattern scoped to a material). Both are albedo sockets `f(points)→rgb` carried on the scene object; the renderer samples them per hit.* + +![Hot metal glowing by temperature](gallery/render_hot_metal.png) + +*__Thermal emission__ — a material glows because it is __hot__, and the colour of the glow is set by its temperature (Planck's law / blackbody radiation): dull red near 700 K, orange by ~1400 K, yellow-white past ~2500 K. Left to right the same iron bars are heated to increasing temperatures, so the blackbody ramp reads as a row. The emission is __derived__ from the material's temperature (`matlib.heat` + `holographic_blackbody`), not a hand-picked colour — a physical property driving the render.* + +![Smoke and fire from one fluid sim](gallery/render_smoke_fire.png) + +*Smoke (left) and **fire** (right) from **one** 3-D Stable-Fluids simulation, rendered volumetrically. A heated plume is simulated on a voxel grid, then a trilinear sampler exposes that grid as the callable density field the volume ray-marcher (`holographic_render.volume_render`) marches: the smoke pass reads it as grey absorption, the fire pass reads the hot (density×temperature) core through a blackbody emission ramp. Both pieces existed; this is the first time the solver and the volume renderer were pointed at each other for a picture.* + +![Fur composited over a path-traced body by the pipeline](gallery/render_fur_over_scene.png) + +*__Hair as a pipeline stage__ — a groomed coat composited over a __path-traced__ body by the render pipeline, not by the hair renderer alone. The body is ray-traced with a library skin material; the pipeline's hair stage then renders the strands (Marschner fiber shading, the look driven by a fur material's physical parameters) __with a coverage alpha__ and over-composites them, so the fur sits on a properly shaded, shadowed body. The strand renderer already existed — the alpha is what lets it be a layer in the frame.* + +![Ember sparks composited over a scene by the pipeline](gallery/render_sparks_over_scene.png) + +*__Particles as a pipeline stage__ — a swarm of glowing ember sparks composited over the scene by the render __pipeline__. The particle system simulates the points (a buoyant drift advanced by the shared symplectic integrator); the pipeline's particle stage projects each point through the camera and splats it as a soft round dot, over-compositing onto the surface render. Nearer sparks cover farther ones and a depth fade dims the ones drifting to the back. The simulator already existed — this is the missing renderer that turns its points into a picture.* + +![Smoke composited over a solid scene by the pipeline](gallery/render_smoke_over_scene.png) + +*__Volume as a pipeline stage__ — a smoke plume rising behind two spheres, composited over the solid scene by the render __pipeline__, not hand-composited in the demo. A little 3-D smoke sim is handed to the pipeline as part of the scene; the pipeline renders the surfaces, then its volume stage marches the smoke density and over-composites it (`out = volume + surface·(1−alpha)`). This is the difference between "the volume renderer exists" and "a scene with a volume renders as one frame."* + +![Subsurface scattering: a backlit honey blob glowing orange where thin](gallery/render_subsurface.png) + +*__Subsurface scattering__ — a translucent material glows where it is __thin__. One big blob of __honey__ (an orange translucent from the library) fills the frame in a near-black room, lit by a single light behind and to the side; its displaced (bumpy) surface varies in thickness, so the thin bumps on the lit side glow bright orange while the thick body absorbs toward black. The path tracer measures how much solid the light crosses inside the object toward the light (`holographic_raymarch.subsurface` — Beer-Lambert on the SDF interior) and adds that as the glow; the colour is the material's own. Fixed exposure on purpose: auto-exposure would lift the dark room to mid-grey and wash out exactly this contrast.* + +> **How these are rendered — and an honest benchmark.** The old gallery called the raw path tracer at a fixed> sample count, so it was grainy in the hard spots (glass, reflections) and wasteful in the easy ones (flat sky). +> `render_auto` fixes the *wiring*: it converges each pixel to a quality target and denoises by the measured +> variance. Measured on the spheres scene against a 128-spp reference (PSNR in tonemap space — where visible +> grain lives): at equal average sample budget the auto path **beats** a raw trace at draft/medium quality +> (e.g. +2.4 dB at ~10 spp, +0.8 dB at ~19 spp), and **ties** it near convergence at high quality — because once +> a pixel is already converged, denoising it can only soften detail (a documented crossover, kept loud). The win +> is that this happens with *no per-scene tuning*: the same call calibrates spheres, glass, a fractal, and a +> water tank alike. + +![A composed texture painted onto a described scene](gallery/render_composed_texture.png) + +*The **composability** stack, made visual — a different path from the tracer above. A texture is built as a small +graph of operations (`mind.texture_op("mix", a=red, b=cyan, t=fbm_noise)`), a scene is described in words +(`mind.build_scene("a big sphere and a small box")`), the graphs are painted on (`scene.paint(...)`), and +`scene.render()` routes through `render_textured`: it marches the SDF, turns each surface hit into a UV +coordinate (spherical on the sphere, planar on the box), samples the composed texture there, and shades it with +the same Cook-Torrance BRDF plus a light and a hard shadow. The pattern genuinely **wraps** the geometry via UV +mapping — it isn't a flat recolour. (Honest: textbook UV, so a seam and a pole pinch on the sphere; one hard +light — the path tracer above is the tool for soft GI.)* + +--- + +## Procedural & generative + +Richness from a tiny deterministic kernel. + +![Procedural pattern fields](gallery/patterns.png) + +*Procedural pattern fields (`holographic_pattern`): fBm, value noise, checker, dots — each a **field over world +position**, a solid 3-D texture that wraps any surface with no UV unwrap.* + +![Vector reaction–diffusion](gallery/reaction_diffusion.png) + +*A vector-valued reaction–diffusion cellular automaton (`holographic_automaton`) — Turing patterns in hypervector +space, 24 steps, projected to RGB.* + +![A zoo of reaction–diffusion patterns](gallery/ca_zoo.png) + +*The same machinery under different couplings — from the test suite.* + +![Film grain / image processing](gallery/film_grain.png) + +*Image-processing output from the test suite.* + +--- + +## Content-addressable memory & superposition + +Store many things in one space; recall by content; degrade gracefully instead of failing hard. + +![The holographic image archive](gallery/archive.png) + +*The holographic image archive (`holographic_archive`): images superposed into Walsh–Hadamard key plates and any +one recovered by content — exact when undamaged, graceful under an erasure mask.* + +![Holographic image reconstruction](gallery/holo_capable.png) + +*Reconstruction quality across stored images — recovered beside original, from the test harness.* + +![Superposition multiplexing](gallery/multiplex.png) + +*Many signals bundled into one hypervector and pulled back apart by content — superposition as storage.* + +--- + +## The deterministic learning creature + +![The learning creature](gallery/creature_viz.png) + +*The reinforcement-learning forager (`holographic_creature`) — deterministic, debuggable, learns online without +catastrophic forgetting, and can say in human sense-terms why it chose a move.* + +--- + +## Data & measured behaviour (the non-3-D story) + +How the algebra actually behaves — every claim with a baseline and a spread. These four are generated fresh. + +![Core op cost vs dimension](gallery/perf_core_ops.png) + +*Cost of the two core operations vs hypervector dimension — `bind` (FFT circular convolution) and a 16-way +`bundle` (superposition), microseconds per op, single thread. The whole algebra is cheap.* + +![Compression vs SQL](gallery/compression_vs_sql.png) + +*The measured answer to "how well does our store compress vs SQL?" — bytes/record for the engine's low-rank +rate-distortion code vs SQLite on the same structured data, as the table grows. The VSA store's shared basis +**amortises**, so per-record cost **falls with N** and crosses *under* SQLite at a few thousand rows (~10 vs +~32 B/record by 50 k). gzip beats both on raw bytes, but gives no query — the VSA bytes *are* the fuzzy index.* + +![Memory capacity curve](gallery/capacity_curve.png) + +*Key→value recall accuracy vs how many pairs are packed into one vector, at three dimensions. The honest capacity +**cliff** — and how adding dimensions moves it to the right (capacity ≈ order D).* + +![Graceful degradation under corruption](gallery/graceful_degradation.png) + +*Recall accuracy as the memory vector is progressively zeroed out. It declines **gracefully** rather than +crashing — the hallmark of distributed/holographic storage.* + +![Capacity vs load (harness)](gallery/bench_capacity.png) +![Recall under corruption (harness)](gallery/bench_corruption.png) +![Quantization robustness (harness)](gallery/quant_robust.png) +![Throughput (harness)](gallery/bench_throughput.png) +![Scaling under stress (harness)](gallery/stress_scaling.png) +![An ablation result (harness)](gallery/improve_final.png) + +*More measured curves from the benchmark/stress harness — capacity, corruption tolerance, quantization +robustness, throughput, adversarial scaling, and a before/after ablation. Every gain is measured against a proper +baseline.* + +--- + +*The 3-D renders, procedural patterns, reaction–diffusion frame, and the four data charts regenerate any time +with `python make_gallery.py`; the rest are produced by the test suite and benchmark harness.* diff --git a/HOLOGRAPHIC_FUNCTIONS.md b/HOLOGRAPHIC_FUNCTIONS.md new file mode 100644 index 00000000..b6bdf444 --- /dev/null +++ b/HOLOGRAPHIC_FUNCTIONS.md @@ -0,0 +1,142 @@ +# holostuff — The Panel on Inception: Functions, Folders, and What the OS Layer Unlocks + +*Gathered a second time on the inception thread, now that the OS rung exists (`HoloMachine`: a program +encoded as one hypervector, executed by VSA ops). The questions on the table: can we embed and execute +**functions** inside the holographic space rather than as Python files? Does that buy extra abilities? +Can **folders/partitions** cut confusion? How useful is any of this — and what does it let us do that we +never planned for? As always, every view is attributed to a **seat** and that field's real, published +methods, and every claim below was **measured on the substrate first**.* + +--- + +## The short answers (all measured) + +| Your question | The measured answer | +|---|---| +| Functions inside the holographic space, not as Python files? | **Yes, two ways** — *demonstrated* mappings and *callable* library subroutines. | +| Extra abilities? | **Yes** — learned-by-example functions, content-addressable-by-behavior, function arithmetic. | +| Folders/partitions to avoid confusion? | **Yes** — at 256 items, flat recall 86% → 16-folder recall **100%**. | +| How useful? | Useful where *deterministic, inspectable, composable, content-addressable* code matters — not as a fast CPU. | +| What didn't we plan for? | Retrieving code **by what it does**, and **averaging two programs** like vectors. | +| How does it improve us? | Code and data share one algebra, so **every engine faculty now applies to programs too**. | + +--- + +## 1. Functions embedded in the holographic space — two kinds (Plate; Adamatzky) + +**The HRR-foundation seat (Plate)** points out that the substrate is *homoiconic*: a program and the +data it touches are the same kind of object (`HoloMachine` already puts instructions and operands in one +vector). That makes two genuinely different notions of "function" available, neither of which is a +Python file. + +**(a) A function you *demonstrate* instead of write.** A mapping `f: key → value` is stored as a single +vector `M = bundle_i bind(key_i, value_i)`, and applied by `f(k) = cleanup(unbind(M, k))`. You never +write the logic; you give examples, and the vector *is* the function. Measured: **100% correct up to +~120 pairs** at dim 4096, cliff at ~240 (87%). This is `HolographicMemory` seen as what it always was — +a learned, content-addressable function. (It is also Plate's classic "holographic mapping.") + +**(b) A function you *call*.** An `ACC → ACC` sub-program (e.g. `BIND b; HALT`) is embedded into a +single **library vector** under its name, and invoked with a new `CALL` opcode: the body is pulled out +of the library by name (`unbind`) and run on the current accumulator. Measured: `LOAD a; CALL tag_b; +CALL shift` produces `permute(bind(a,b))` at **cosine 1.0**, with all functions living inside one +library vector. Functions compose like ordinary code — and *are* ordinary data. + +**The unconventional-computing seat (Adamatzky)** is the natural home for this: his field is computing +in substrates that are not von Neumann CPUs (slime moulds, reaction–diffusion). A machine whose code, +data, and memory are all the same hypervector algebra is exactly that — computation as an emergent +property of a representational medium, not of a fetch-decode-execute chip. + +--- + +## 2. Extra abilities — including what we never planned for (Eno; Togelius; Pharr) + +**The generative-art / reframe seat (Eno)** is interested less in the planned features than in the ones +that fall out for free because code now lives in a vector space: + +- **Content-addressing by behavior.** Give an *example of what you want* — input `a`, desired output + `permute(a)` — and the matching function ("shift") is retrieved by a behavioral signature, not by + name. Measured, correct. You can search code by what it *does*. +- **Function arithmetic.** `bundle(f1, f2)` is a function that carries *both* answers (measured 0.18 / + 0.18, symmetric). You can average, blend, and interpolate programs the way you blend vectors — a + capability no file-based codebase has. Eno's "honour thy error" instinct applies directly: a blended + or slightly-corrupted program degrades gracefully rather than crashing. + +**The game-AI seat (Togelius)** sees the payoff for agents: an NPC's policy can be a *portable program +vector* — content-addressable, composable, blendable, and inspectable — rather than a compiled +controller. **The data-structure seat (Pharr)** sees the dual: a library of functions indexed for +sub-linear retrieval (the `HoloForest`) means "find the routine closest to this behavior" is a +nearest-neighbour query, not a grep. + +--- + +## 3. Folders and partitions — yes, and the engine already has them (Pharr; Macklin) + +The "confusion" in a holographic store is **crosstalk**: bundle too much together and cleanup starts +matching the wrong thing. **Folders are the cure, and they are already a primitive** — +`PartitionedMemory` routes each key to its own subspace, so a query competes only against its folder's +contents, not the whole drive. Measured at fixed load: + +| total items | flat store | 16 folders | +|---|---|---| +| 64 | 100% | 100% | +| 128 | 100% | 100% | +| **256** | **86%** | **100%** | + +So partitions directly buy back the capacity that a flat store loses to crosstalk — and they give +**namespacing** for free (two functions named `f` in different folders never collide, because they hang +off different partition roles). The data-structure seat (Pharr) reads this as a spatial hash for +meaning; the constraint-solver seat (Macklin) reads it as decoupling — isolate sub-problems so their +errors don't leak into each other. + +--- + +## 4. How useful is this, honestly? + +The honest boundary matters as much as the capability. This is **not a fast general-purpose CPU** — +Python runs these programs far faster than the holographic interpreter does, and nobody should port a +hot loop into hypervectors. Its edge is the same edge the whole engine has: **deterministic, +inspectable, composable, content-addressable code-as-data.** It is useful exactly where those +properties are the point: + +- a **portable agent/policy** that travels as one vector and can be blended or recalled by behavior; +- a **self-describing record** that carries its own decode/validate routine in the same vector as its + data; +- a **sandboxed mini-interpreter** whose entire state is one inspectable object; +- a **case library** where "what did we do last time something looked like this?" is a recall, and the + answer is itself runnable. + +--- + +## 5. How it improves our ability to do things — the multiplier (every seat) + +This is the part the whole room agreed is the real prize. Because code and data now share **one +algebra**, every faculty the engine already has applies to programs, with no new machinery: + +- **Consolidation (Stoudenmire/Duda seats)** can *compress a program* — a library of related functions + has low-rank structure, so it consolidates like any other state. +- **Denoising (Milanfar seat)** can *clean a corrupted program* — cleanup already rescues noisy + instruction reads; the same operator repairs a damaged library. +- **The resonator / factorizer (Olshausen seat)** can *factor a program into its parts* — decompose a + composed behavior into the sub-functions that built it. +- **The forest (Pharr seat)** can *index programs* for sub-linear retrieval by behavior. +- **The generative sampler (Eno seat, B10)** can *generate new programs* — sample over the "program + manifold" to propose novel-but-valid routines. + +That is the answer to "how does this improve our ability": it **collapses the wall between what computes +and what is computed on.** A binding is a multiply is a function application; a bundle is a sum is a +library is a blend. The same five primitives that store a scene now store, retrieve, compose, repair, +and generate the programs that build scenes. We did not plan most of that — it is what falls out when +you take the OS rung seriously and notice the code was already the same stuff as the data. + +--- + +### What shipped this round (measured, with kept boundaries) +`holographic_machine.py` gained a `CALL` opcode, `HoloMachine.define(name, program)` (embed a named +function into one library vector), and `run(..., init_acc=...)` (so functions are composable ACC→ACC +transforms) — backward-compatible, 4 new tests (603 total). The demonstrated-function and folder +capabilities use existing primitives (`HolographicMemory`, `PartitionedMemory`), so they were measured +and named rather than re-implemented. Honest boundary kept on the record: finite capacity (functions +~160 pairs, library subroutines bounded by the same bundling crosstalk), and no claim to CPU speed. + +*(Still teed up from the previous session, not forgotten: adaptive-rank denoising to cash B7's +low-noise kept negative. This round answered the inception questions; that one is next when you want it.)* diff --git a/HOLOGRAPHIC_INCEPTION.md b/HOLOGRAPHIC_INCEPTION.md new file mode 100644 index 00000000..e5ca42fc --- /dev/null +++ b/HOLOGRAPHIC_INCEPTION.md @@ -0,0 +1,106 @@ +# Holographic inception: how deep does the structure go? + +*Prompted by a question: a hard drive has physical structure, and data represented by that structure, +which when executed runs an OS, which can host a VM, which can host another OS. holostuff has the first +two rungs. Can we go deeper? How far can the inception go, and how do we "format the drive"?* + +The answer is yes, and the reason is the one property that makes a holographic substrate special: +**the thing you store and the space you store it in are the same kind of object.** A hypervector can +hold a value, a record, a whole nested scene — or the *recipe* for building those, executed in place. +That is exactly the hard-drive tower: structure, data in the structure, and data that becomes new +structure when run. Below is the stack, where each rung sits in the engine, and — measured — how far +down it goes. + +--- + +## The stack + +| Hard-drive layer | holostuff rung | Status | +|---|---|---| +| Platter (physical structure) | the D-dimensional float vector | already the substrate | +| Low-level format | `derived_atom(seed, name, …)` — a deterministic alphabet | already here | +| File system | role–filler records (`bind`+`bundle`), nested directories (`compose_nested`) | already here | +| **OS that executes** | **`HoloMachine` — a program encoded as a vector, run by VSA ops** | **new this round** | +| VM inside the OS | the same nesting, applied to executable structure | measured (a depth law) | + +### "Formatting the drive" +Formatting is just fixing a seed. `derived_atom(seed, …)` lays down the entire alphabet +deterministically: two roles (`OP`, `ARG`), a `SLOT` role for nesting, the opcode atoms, the data +atoms, and an address function `POS(i)`. Two machines with the same seed agree bit-for-bit — the +format is reproducible, which is the whole point of a format. Nothing here is learned or random at +run time; it is a layout. + +### The file system was already there +A "file" is a role-filler record: `bundle(bind(key₁, val₁), bind(key₂, val₂), …)`, read back by +unbinding a key. A "directory" is a bundle of named files. A "path" is a chain of unbinds. Nested +directories — directories inside directories — are exactly `compose_nested`, which the engine already +had (measured ceiling ~1.0 at two groups, ~0.97 at three). So the file-system rung needed no new code; +it needed only to be *named*. + +### The new rung: an OS that executes +The missing piece is an interpreter — something that treats a stored vector as a *program* and runs +it. `HoloMachine` does that with a deliberately tiny, readable instruction set: + +``` +LOAD x : ACC = x BUNDLE x : ACC = bundle([ACC, x]) +BIND x : ACC = bind(ACC, x) PERMUTE : ACC = permute(ACC, 1) +HALT : stop +``` + +A program is a list of `(opcode, operand)`. It is assembled into **one** hypervector: + +``` +instruction_i = bundle( bind(OP, opcode_i), bind(ARG, operand_i) ) +program = bundle_i ( bind(POS(i), instruction_i) ) +``` + +Instructions and data live in the same vector space — von Neumann architecture, holographically. To +run it, the interpreter unbinds each address `POS(i)`, **cleans** the opcode and operand against their +codebooks (a wide-margin classification, robust to the crosstalk from all the other instructions +bundled into the same vector), and dispatches. Because the operand is cleaned to an *exact* atom +before use, the accumulator is built from clean atoms and stays **exact** even though reading the +program is noisy. + +Measured: `LOAD a; BIND b; BUNDLE c` produces `ACC == bundle(bind(a,b), c)` at cosine **1.0000**, with +the decoded instruction trace exactly correct. The substrate executed a stored program. + +--- + +## How far does it go? Two measured cliffs + +### Drive size — how big a program fits +Every instruction adds two more bound terms to the same bundle, so eventually the crosstalk +overwhelms cleanup. Instruction-decode accuracy versus program length, at two dimensions: + +- dim **1024**: ~100% to ~32 instructions, then the cliff (80% by 64). +- dim **4096**: ~100% to ~128 instructions, then the cliff (87% by 192). + +Capacity is finite and scales with dimension — quadruple the dimension, roughly quadruple the program. +This is the honest HRR capacity wall, kept on the record rather than hidden. + +### Inception depth — how deep the nesting goes +A program is just another value, so it can be stored as a "file" on a "disk" (a bundle), and that disk +can itself be the file on a higher disk — inception. How deep before the buried program stops running? +It depends entirely on **how cluttered each level is**: + +- **Clean nesting** (the program is the only file at each level): runs correctly at depth **8 and + beyond**. A pure chain of unitary bind/unbind barely degrades, so you can nest almost arbitrarily + deep. +- **Busy disk** (each level also holds other files): the buried program corrupts after only **~3–4 + levels**, because every level adds crosstalk from its neighbours. + +That is the law, and it is a satisfying one: *you can go as deep as you like if each level is +uncluttered; a crowded disk corrupts a buried program after a few levels.* Exactly like a real drive — +the more you cram in alongside, the sooner the thing underneath is unreadable. Both limits scale with +dimension, so a wider substrate is a bigger drive that nests deeper. + +--- + +## So: how far can the inception go? + +As far as you are willing to spend dimensions on. The tower is real — platter, format, file system, +an OS that executes, and an OS-inside-the-VM by nesting — and every rung is the *same* handful of +primitives (`bind`, `bundle`, `cleanup`, `permute`, `derived_atom`) pointed at itself. The only thing +that ends the recursion is noise, and noise is bought off with dimension: a finite but honest budget, +measured at every level. We were indeed at the very beginning of that tree. We are now a few rungs up +it, with the rungs counted. diff --git a/ISA.md b/ISA.md new file mode 100644 index 00000000..be661845 --- /dev/null +++ b/ISA.md @@ -0,0 +1,151 @@ +# ISA.md — the holostuff VSA instruction-set contract + +*The frozen, observable semantics of the base instructions. This is the **architecture**; the FFT / BLAS / +forest implementations are **microarchitecture** below it. This document is the contract ISA-2's conformance +suite enforces, and the one place the determinism rules are stated (the executable copy lives in +`holographic_determinism.py`, which `spectral` and `chart` cite). Grounded in the live kernel +(`holographic_ai.py`), not memory.* + +--- + +## Why a written contract (the bind_batch lesson) + +An ISA is durable only if the exact **observable** semantics of its base operations are frozen while the +implementations vary underneath — that is the whole reason x86 outlived the chips that ran it. The `bind_batch` +bug is the cautionary tale in this codebase: a microarchitecture change (batched BLAS, bit-exact to 1e-12) +flipped a creature's maze trajectory, because it changed a summation order that fed a downstream `argmax` whose +tie-break the contract never pinned. The change was numerically innocent and behaviourally fatal. The lesson is +not "never vectorize" — it is **"write down the observable decision and pin it; let the continuous numbers +vary within a stated tolerance."** + +## The architecture / microarchitecture boundary + +- **ARCHITECTURE (pinned EXACTLY).** The observable decision a caller depends on: *which* atom `cleanup` + returns; that `permute` and `involution` invert exactly; that `bundle`/`cosine` return the documented value + on the zero-vector edge. A conformant implementation must reproduce these bit-for-bit where the result is a + decision or an exact reindex. +- **MICROARCHITECTURE (may vary within a numeric tolerance).** *How* the continuous numbers are computed: an + FFT vs a direct circular convolution, a batched vs a looped reduction. No caller can observe the last bit of + a reduction — only the decision it feeds — so these may differ within tolerance, **provided the decision they + feed is unchanged.** `bind_batch` is exactly such a microarchitecture variant of `bind`. + +## The one determinism rule (stated once; executable in `holographic_determinism.py`) + +1. **Argmax tie-break → lowest index.** Every `argmax`-style decision (`cleanup`, recall) resolves an exact tie + to the lowest index. This is numpy's `argmax` convention, named `argmax_tiebreak` so it is citable. +2. **Eigenvector / embedding sign → largest-magnitude entry positive.** Any eigenbasis or spectral embedding + has each column's largest-|entry| made non-negative (`fix_eigvec_signs`), removing `eigh`'s sign ambiguity. + *Does not* resolve the basis within a degenerate eigenspace — a documented deeper limit. +3. **Reductions feeding a decision run in a fixed, documented order.** Where a downstream decision depends on a + summation order (the bind_batch class), the order is part of the contract and pinned by a conformance test; + where no decision depends on it, the order is microarchitecture and free. + +--- + +## The base instructions + +Each entry: signature · observable semantics · exactness class (EXACT = bit-for-bit / a decision; TOL = a +continuous value, conformant within numeric tolerance) · edge cases. + +### `random_vector(dim, rng)` — mint an atom +- **Semantics.** A fresh unit-norm vector drawn from `rng`. The atom alphabet is whatever a *seeded* rng emits. +- **Class.** EXACT given `(dim, rng_state)` — the same seed yields the same atoms, so codebooks are reproducible + (the engine's determinism rests on this). The *values* are microarchitecture; the *sequence for a fixed seed* + is architecture. +- **Edges.** `dim > 0`. The rng is the deterministic state, never the global numpy RNG. + +### `bind(a, b)` — associate (circular convolution via FFT) +- **Semantics.** Combine two vectors into a composite dissimilar to both; **commutative**; the inverse of + `unbind`. `bind(a,b)` followed by `unbind(·, a)` recovers `b` approximately. +- **Class.** TOL. The composite is a continuous value; an FFT and a direct convolution agree within tolerance. + **`bind_batch(A,B)` is the vectorised microarchitecture variant** — same convolution over stacked rows, + conformant within tolerance, but **NOT pinned bit-for-bit to the looped `bind`**, which is exactly why it was + kept out of the tie-sensitive creature path: it can only sit in front of a decision if that decision is pinned. +- **Edges.** `a`, `b` share `dim`. + +### `unbind(composite, a)` — recover the bound partner +- **Semantics.** `bind(composite, involution(a))` — recovers `b` from a composite containing `bind(a,b)`. + Recovery is **approximate**: its fidelity is bounded by how loaded the composite is (the capacity cliff). +- **Class.** TOL. The architectural guarantee is *approximate* recovery (high cosine to `b` for clean atoms at + modest load), not an exact value. +- **Edges.** Same `dim`; quality degrades as more is bound/bundled in — a capacity question, not an error. + +### `involution(a)` — the stable inverse for unbinding +- **Semantics.** The reversal used to invert `bind`. **Exactly self-inverse**: `involution(involution(a)) == a`. +- **Class.** EXACT (a reindex/conjugation, no float drift). +- **Edges.** None beyond `dim`. + +### `bundle(vectors)` — superpose (and renormalize) +- **Semantics.** Sum the vectors and renormalize; the result stays *similar* to each part (how one vector stands + for a set). **Order-independent up to float summation order.** **Information-destroying** — there is no exact + inverse (flagged for ISA-8's reversibility audit). +- **Class.** TOL for the continuous value. **EXACT edge:** a zero-sum bundle (e.g. `a` and `-a`) returns the + **zero vector**, not a divide-by-zero (`return total/norm if norm > 0 else total`). +- **Edges.** Empty input is caller error; the zero-sum case is pinned above. + +### `cosine(a, b)` — similarity +- **Semantics.** `dot(a,b) / (‖a‖·‖b‖)`; 1.0 identical, ~0 unrelated. Drives every recall decision. +- **Class.** TOL for the value. **EXACT edge:** if either norm is 0, returns **`0.0`** (no divide-by-zero). +- **Edges.** The zero-norm case is pinned above. + +### `permute(vec, shift)` — cyclic shift (encode order/position) +- **Semantics.** `np.roll(vec, shift)` — a cyclic reindex; the result is dissimilar to the original and the + shift is reversible (`permute(·, -shift)`). The VSA primitive for order; the stack-push for ISA-5. +- **Class.** EXACT (a pure reindex — bit-for-bit, invertible, no float error). +- **Edges.** `shift` is taken mod `dim`. + +### The `cleanup` decision — nearest atom +- **Semantics.** Return the codebook atom of highest cosine to the query (`int(sims.argmax())`). +- **Class.** EXACT decision. Pinned by rule 1 (lowest-index tie-break, `argmax_tiebreak`). Two implementations + of the similarity scan may differ in the last bit of the dot products (microarchitecture) but **must** agree + on this index (architecture), with exact ties going to the lowest index. + +--- + +## How this is enforced (ISA-2) + +A conformance suite (`test_isa_conformance.py`, ISA-2) gives the contract teeth: per instruction, a definitional +reference implementation plus golden vectors; any implementation (the FFT `bind`, `bind_batch`, a future batched +`bundle`) must match the reference **within tolerance on TOL outputs and exactly on EXACT outputs/decisions**. +It includes a regression for the bind_batch class itself — a deliberately summation-reordered op that flips a +decision must FAIL the suite. Until ISA-2 lands, `holographic_determinism._selftest` and the spectral/chart +tests pin the determinism rules; the per-instruction golden vectors are ISA-2's job. + +## What this contract deliberately does NOT freeze + +Per the ISA-1 negative: only the **observable** semantics above are contract. The FFT's internal rounding, the +exact bits of a reduction no decision depends on, and the basis within a degenerate eigenspace are **not** +frozen — pinning them would mistake incidental float behaviour for architecture and block legitimate +optimization (the very `bind_batch` speed-up the contract exists to make safe). + +--- + +## The calling convention (ISA-5 — the ABI) + +`CALL f` runs the named library function `f` on the current accumulator. The convention that makes nesting and +recursion well-defined, now that the machine has registers (ISA-4) and a stack (ISA-5): + +- **ACC is the argument and the return value.** A function is an ACC→ACC transform: it receives the caller's + ACC (`init_acc`) and the value it leaves in ACC is what the caller continues with. The whole function library + obeys this — every defined function reads its input from ACC and leaves its output there. +- **Registers and the stack are FRAME-LOCAL.** Each `CALL` runs in its own frame with a fresh register file + (R0–R7) and a fresh permute-stack. A callee therefore **cannot corrupt the caller's registers or stack** — + they are preserved across the call automatically (measured: a callee that overwrites its R0 leaves the + caller's R0 bit-identical, cosine 1.000). In ABI terms every register is effectively callee-saved by + construction: the caller need not spill anything to keep a value across a `CALL`. +- **Recursion is depth-guarded.** Self-reference (a function that `CALL`s itself, with an `IFMATCH` base case) + recurses under a fixed depth guard (8) so a missing base case cannot run away. + +### The permute-stack and its safe depth (the kept negative) + +The permute-stack (`PUSH` / `POP`) is a LIFO **in the vector substrate**: `PUSH` is permute+bundle (shift the +existing items one level deeper, drop ACC on top); `POP` is cleanup+inverse-permute (the top is the only +un-permuted term). It is the explicit-stack form of recursion — e.g. reversing a sequence by pushing every +element then popping — and it runs correctly **at shallow depth**. + +But it is a *holographic* stack: every level rides one bundle, so depth is bounded by crosstalk exactly like the +B8 iterated-decode cliff. **Measured safe depth: ~4–8 items at dim 1024** (LIFO recovery 1.00 to depth 4, ~0.92 +at 8, ~0.48 by 16; a little deeper at dim 4096). So the permute-stack is for shallow nesting of cleanup-able +items; for arbitrary intermediates at any depth, use the registers (exact, frame-local). This is the same +capacity lesson the bundled register file taught (ISA-4): superposition buys composability and pays in a +crosstalk cliff — measure it, and keep the exact path for what needs to be exact. diff --git a/ISA_EXTENSIONS.md b/ISA_EXTENSIONS.md new file mode 100644 index 00000000..98452058 --- /dev/null +++ b/ISA_EXTENSIONS.md @@ -0,0 +1,102 @@ +# ISA_EXTENSIONS.md — the governed bind-mode extensions + +*The VSA analog of x86 + SSE/AVX/AES-NI: a minimal **base** instruction set (ISA.md) plus named, opt-in +**extensions**, each justified by a measured regime win over base `bind`. The base stays RISC; specialty +operations live in governed extensions that must earn their place. Grounded in the live modules and in fresh +measurements (this session), not memory.* + +--- + +## The model, and the standing rule + +Real instruction sets grow as **base + extensions**, never by bloating the base. holostuff already does this by +instinct — the Clifford module's own docstring states the rule ("a parallel mode whose seat is a regime where it +*measurably* beats convolution"). This document makes it policy: + +> **The base kernel stays minimal. A new bind mode is an EXTENSION, opt-in, in its own module, and must earn its +> place with a measured regime win over base `bind` on real data — with its cost and kept negative stated.** + +## The base/extension boundary (the principle, applied) + +The debatable case is `permute`: is it base or extension? The principle decides it: + +> **BASE = what (almost) every faculty uses — the `holographic_ai.py` kernel. EXTENSION = regime-specific — a +> separate, opt-in module.** + +By that rule the **base instruction set is frozen as** (full semantics in ISA.md): `random_vector`, `bind` +(FFT circular convolution), `unbind`, `bundle`, `permute`, `cosine`, `involution`, and the `cleanup` decision. +`permute` is **base** — it lives in the kernel and is used across the sequence, creature, and structure +faculties for order. The three extensions below are **not** base: each is a separate module, opt-in, serving a +specific data type or task the base does not. + +--- + +## Extension 1 — Clifford-bind (the geometric product) `holographic_clifford.py` + +- **Regime.** Geometric structure, specifically **3-D rotations** and other order-sensitive (non-commutative) + composition. Cl(3,0): a multivector is an 8-vector; the geometric product is the bind. +- **Measured win.** **Rotation composition is EXACT and is one product.** The geometric product of two rotors + *is* the rotor of the composed rotation, so composing then applying equals applying sequentially — measured + error **1.1e-16**. And the product is **non-commutative**, so it captures order where base `bind` (commutative + convolution) provably cannot. Base convolution has no way to compose two rotations and apply exactly. +- **Cost / kept negative.** Cl(d) is **2^d-dimensional** — fine for Cl(3,0) (8 numbers) but a 2^d blow-up rules + it out as a *general* high-D substrate. Use it where the structure is genuinely rotational/geometric; base + `bind` (one FFT) remains the efficient default everywhere else. +- **Conformance** (`test_isa_extensions.py`): compose-then-apply equals sequential application to ~1e-15; a + rotor application is length-preserving and exactly invertible. + +## Extension 2 — FPE / VFA (fractional power encoding) `holographic_fpe.py` + +- **Regime.** **Continuous / spatial values** — encode a real quantity so that *nearby values are similar*, + with the similarity profile a kernel you DESIGN (Bochner's theorem: the kernel is the phase distribution's + characteristic function — RBF for Gaussian phases, sinc for uniform). +- **Measured win.** The designed kernel gives a **smooth, monotone similarity falloff** over continuous offset + (cosine **1.0 → 0.9 → 0.66 → 0.41 → 0.22 → 0.11 → 0.04** across offsets 0…3 with an RBF kernel), where + independent random atoms for the same values have **no continuity at all** (all ≈ 0 off the diagonal). Shift + in a coordinate is a binding; the n-D kernel factors across axes. +- **Cost / kept negative.** It is an **encoder, not a general bind** — the kernel is a design choice (the + bandwidth), and the value is the *geometry it imposes on continuous inputs*, not a replacement for `bind` on + discrete atoms (where random near-orthogonal atoms are exactly what you want). +- **Conformance** (`test_isa_extensions.py`): the kernel falls off monotonically with offset and stays well + above the random-atom baseline; the peak is at the encoded value. + +## Extension 3 — Tensor-product bind (outer product / MPS) `holographic_tensor.py` + +- **Regime.** **High capacity / exact unbinding** when you can afford the storage. HRR's `bind` is a + *compressed projection* of Smolensky's tensor-product binding; this keeps the uncompressed outer product + (and a tensor-train / MPS truncation in between). +- **Measured win.** At a load that **overloads HRR**, the tensor memory recalls cleanly where convolution drowns + in crosstalk: at 12 pairs, D=32, **HRR recall 0.29 vs tensor recall 0.87**. The outer-product store unbinds a + stored pair near-exactly. +- **Cost / kept negative.** The storage is **D² numbers** (vs D for convolution) — the win is bought with + dimension. And a generic full-rank binding **cannot be MPS-compressed without losing recall**, so the + tensor-train middle ground only helps for low-entanglement structure. The frontier is + `HRR (D) < tensor-train (≈2rD) < full tensor product (D²)` — a storage/fidelity tradeoff, not a free win. +- **Conformance** (`test_isa_extensions.py`): at a fixed overloading load, tensor recall exceeds HRR recall; a + single stored pair round-trips near-exactly. + +--- + +## The new-extension proposal template (the earning-its-place bar) + +A new bind mode is admitted as an extension only when every line is filled — the same discipline the three above +already pass: + +1. **Name & module.** A separate, opt-in module; the base kernel is **not** touched (verify base ops are + unchanged — `conformance_report()` still passes). +2. **Regime.** The specific data type or task it serves (and the data type where it should *not* be used). +3. **The baseline it must beat.** Base `bind` (or the relevant base op) on that regime — measured, on real data. +4. **The measured win.** A number on the real substrate showing it beats the baseline *in its regime*, with the + regime stated. No claim without a measurement (the project's standing rule). +5. **Its own conformance test.** A test in the extension-conformance suite pinning the win and any exactness + property (composition, round-trip, kernel shape). +6. **Cost & kept negative.** What it costs (dimension, compute, a design choice) and where it does NOT help — + stated as loudly as the win. + +If a proposed mode cannot show a measured regime win, it does not become an extension — the base stays minimal. + +--- + +*The base is RISC and frozen (ISA.md). These three extensions are governed: each names its regime, shows a +measured win this session (Clifford 1.1e-16 exact rotation; FPE 1.0→0.04 designed kernel vs flat random atoms; +tensor 0.87 vs HRR 0.29 at overload), and carries its cost and kept negative. New modes earn in by the same bar.* diff --git a/ISA_REVERSIBLE.md b/ISA_REVERSIBLE.md new file mode 100644 index 00000000..3cbe5deb --- /dev/null +++ b/ISA_REVERSIBLE.md @@ -0,0 +1,75 @@ +# ISA_REVERSIBLE.md — the reversible / error-correction model (ISA-8, the frontier) + +*The last item of the VSA ISA spine, and the most conceptual. It names what the engine has been all along, imports +a discipline from reversible/error-correcting computing, and ships one practical, measured payoff — an auto-cleanup +scheduler. The honest framing matters more here than anywhere else on the spine, so the loud negative comes first.* + +## The loud negative, up front: this is an ANALOGY, not physics + +VSA is **not** a quantum computer. There is no exponential superposition, no physical entanglement, no quantum +speedup. FHRR's `bind` happens to be a diagonal-unitary operator — a per-frequency phase rotation, which is +*structurally* gate-like — and that is a genuinely useful lens for capacity bounds. But it is a lens. What ISA-8 +actually adopts is the **discipline** of reversible/error-correcting computing — track an error budget, correct +before the cliff, keep reversibility bookkeeping — not any claim about the physics. Overclaiming the quantum +connection would be exactly the kind of unmeasured assertion this project exists to refuse. + +So of the three parts below, **(b) the scheduler is the practical, measured core**; (a) the reversibility audit +is doable, testable framing; (c) the quantum-gate connection is conceptual scaffolding. They are labelled as such. + +## (a) The reversibility audit — which instructions are reversible + +VSA assembly is partly reversible. Classifying the base instructions (verified empirically in +`holographic_reversible.py`): + +| Instruction | Class | Why | +|---|---|---| +| `bind` | **reversible** | exact inverse is `unbind` by the same (unitary) key | +| `unbind` | **reversible** | exact inverse is `bind` by the same key | +| `permute` | **reversible** | exact inverse is `permute` by the negated shift | +| `involution` | **reversible** | self-inverse: `involution(involution(x)) == x` | +| `bundle` | **lossy** | a sum — the summands are not exactly recoverable; coherence is spent here | +| `superpose` | **lossy** | a raw sum (no renormalization) — same | +| `cleanup` | **lossy** | projection to the nearest codebook atom — discards the residual | + +The reading that organises everything: the **lossy** instructions are exactly where information (coherence) is +spent or restored, and `cleanup` is *error correction* — it snaps a drifted vector back onto the codebook +manifold, throwing away the accumulated error (and a little signal with it). `capacity` is the coherence budget; +re-anchoring and the coherence-gate are error-correction rounds. This is the same lesson the whole ISA spine kept +re-learning (the bundled disk, the bundled register file, the permute-stack): superposition buys composability +and pays in a crosstalk cliff. + +## (b) The error budget + auto-cleanup scheduler (the measured payoff) + +A long "program" — a sequence of binds/unbinds/perturbations on a vector — accumulates crosstalk and drifts from +the truth, eventually crossing a cliff where `cleanup` would snap to the *wrong* atom. The scheduler inserts a +`cleanup` **before** the cliff, using an **oracle-free health signal**: the cosine of the running vector to its +nearest codebook atom (1.0 on a clean atom, falling as it drifts into no-man's-land — the capacity diagnostic's +SNR proxy). This generalises the shipped coherence-gate from store-*maintenance* to program-*execution*. + +- **adaptive**: clean only when `health < floor` — correct just-in-time, matching cleanups to the actual damage. +- **fixed**: clean every *k* steps regardless. + +**Measured (bursty damage — heavy steps interleaved with calm, the regime where matching damage pays):** the +adaptive scheduler holds the program output above a 0.9 fidelity threshold (frac-below = 0.000) at **5 cleanups**; +the *best fixed cadence that matches that fidelity* (k=3) needs **16** — roughly **a third**, echoing the +coherence-gate's "matched accuracy at ~⅓ the passes." Fixed cadences that try to use fewer cleanups (k=4 → 12, +k=6 → 8) start dropping below the threshold. The win is entirely from matching cleanups to the bursty damage: +clean right after each burst, skip the calm. + +*Kept honest:* under **constant** damage a fixed cadence is already near-optimal, so the adaptive advantage is +specific to **variable** damage rates — exactly when you cannot know the right fixed *k* in advance. And the +health signal is a proxy: it must trigger early enough (a floor near the drift the bursts cause) that the nearest +atom is still the true one when the cleanup fires. + +## (c) The quantum-gate connection (framing only) + +FHRR represents a hypervector as unit-magnitude phasors and binds by multiplying them — a **diagonal unitary**, a +per-frequency phase rotation. That is the same shape as a layer of single-qubit phase gates, which is why FHRR +sits, as the tensor-network seat put it, "a stop on the road from quantum amplitudes to classical hypervectors," +and why unitarity is what makes `bind` exactly invertible (audit row 1). Useful for reasoning about capacity as a +coherence budget — and nothing more. See the loud negative. + +--- + +*Seats: Stoudenmire (quantum-inspired / tensor networks; the FHRR-as-diagonal-unitary framing) + the +FHRR/honesty/coherence threads. The practical core is the scheduler; the rest is the discipline it borrows.* diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..412f1af0 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,11 @@ +# MANIFEST.in -- what goes into the SOURCE distribution (sdist) beyond the .py files. +# +# The wheel gets the runtime data via setup.py's package_data (see there); this file makes sure the same data, +# the license, and the readme also land in the sdist tarball. Kept small and explicit -- readable over clever. + +include README.md +include LICENSE +include requirements.txt + +# the runtime data package (the vendored dictionary + material property JSON the engine reads at runtime) +recursive-include lecore_data * diff --git a/MOBIUS_AND_STRUCTURE.md b/MOBIUS_AND_STRUCTURE.md new file mode 100644 index 00000000..abbea53b --- /dev/null +++ b/MOBIUS_AND_STRUCTURE.md @@ -0,0 +1,138 @@ +# Möbius topology and structure-first computation in holostuff + +*A research note prompted by two questions: (1) would a Möbius strip define some things better than a +circle — things with multiple states or sign-flipping noise? and (2) the fruit-fly connectome that +drove a virtual fly with no training — what does it say about how we structure, and reorganize, what +the engine knows?* + +Both questions turn out to be one question: **does the topology and structure of a representation +carry computation, beyond the contents it stores?** Everything below is measured on the real engine. +Negatives are kept. + +--- + +## Part 1 — When a circle is the wrong shape: Möbius / non-orientable representations + +### The observation behind the question +Circles, sign flips, and noise recur all through holostuff: binding *is* circular convolution; the +phasor work lives on the unit circle; involution flips a vector and applied twice returns it; binary +quantization (a kept negative) hinges on sign. The intuition — that a Möbius strip might fit some of +this better than a circle — is correct, and it has a precise statement. + +### What the literature says +Neural population activity traces out a low-dimensional manifold whose **topology matches the variable +being represented**: a ring (circle) for head direction, a torus for grid cells, and — importantly — +a **Klein bottle / Möbius structure for orientation** in visual cortex (orientation + spatial phase; +Swindale 1996, Tanaka 1995; topological-data-analysis confirmations since). Continuous-attractor +network theory now builds Möbius-band and Klein-bottle attractors explicitly, using a *custom +non-orientable metric* because a circle's metric is simply wrong there (eLife MADE framework, 2025). + +The dividing line is **orientability**. A *directed* angle (a heading, a phase from 0 to 2π) lives on +a circle. An *axial* quantity — where θ and θ+π are the **same state** — does not. An unoriented +line's direction, a nematic/liquid-crystal director, a crystal axis, a phase defined only mod π: for +all of these, θ and θ+π are identical, and the correct base space is the **projective line RP¹**, the +base of the Möbius double-cover. On a circle, θ and θ+π sit at **opposite** points. + +### Measured on the substrate +holostuff binds by circular convolution, so the circle is its native shape. I tested whether that +shape actually hurts axial data, and whether the standard fix helps. + +**Axial data (θ ≡ θ+π).** Encoding the angle directly (a plain circle) versus the **double-angle map +θ → 2θ** (which makes θ and θ+π coincide exactly — this map *is* the 2-to-1 cover of the circle onto +the Möbius base): + +| | similarity(θ, θ+π) | axial recovery error | +|---|---|---| +| naive circle | **−0.22** (says they're far apart — wrong) | **0.470 rad** (≈ 27°) | +| Möbius / double-angle | **+1.00** (recognizes them as identical) | **0.002 rad** | + +When each measurement is reported as θ *or* θ+π at random (the real situation with unoriented data), +the circle is effectively guessing; the Möbius encoding is essentially exact. + +**Sign-flipping data, f(t+T) = −f(t).** A pattern that inverts every period and only returns after +two is *antiperiodic* — a Möbius double-cover in time. Measured: **100% of its energy lives in the odd +harmonics** (the antiperiodic / Möbius subspace); the periodic (circular) component is ~1e-14. The +ordinary circular basis literally cannot see a sign-flipping pattern. This is the concrete form of +"noise that flips sign" — it has its own subspace, and it is not the circle's. + +### What shipped +`holographic_mobius.py`: +- `AxialEncoder` — double-angle phasor encoder; θ and θ+π map to the *same* hypervector (RP¹, not S¹). +- `antiperiodic_fraction`, `antiperiodic_split` — diagnose and extract the sign-flipping component a + circular representation can't hold. + +Six tests pin it (593 total now). The honest scope is a **kept negative**: use these *only* for +genuinely axial or sign-flipping data. On directed data the circle is correct, and the double-angle +encoder deliberately throws away the half-turn distinction — it would wrongly merge a heading with its +reverse. Topology must match the data; this is a tool for when it doesn't. + +### A bonus: it names an old kept negative +Binary quantization (removed from `auto` because it distorted pairwise-similarity geometry) maps +values to ±1 — which is itself a **Z₂ / antipodal (Möbius-like) identification**. That is *exactly* +why it corrupted circular geometry (mean pairwise distance collapses 1.27 → 1.00 on circular points), +and — the flip side — exactly why it would be the *right* move for axial / sign-flip data. The old +negative was a topology mismatch. Now it has a name. + +--- + +## Part 2 — Structure carries computation: the fruit-fly connectome parallel + +### The experiment +The FlyWire consortium published the full adult *Drosophila* connectome (Dorkenwald et al., *Nature* +2024; ~140,000 neurons, ~50M synapses). Shiu et al. (*Nature* 2024) then wired a leaky-integrate-and- +fire model **straight from that connectome — no training, no reward, no reinforcement learning** — and +it reproduced sensorimotor behavior (sugar → feeding, touch → grooming) at ~95% accuracy. In 2026 an +embodied version drove a physics-simulated fly body (walking, grooming) from the wiring alone. + +The honest, load-bearing claim across all the coverage (and the careful critiques): **structure +carries computation.** Biological wiring beat random graphs and standard neural-net controls. The +architecture itself holds the work — not a trained set of weights. + +### Why this is holostuff's thesis +holostuff is deterministic and structure-first: no backprop, no gradient training. The bind/bundle +**organization** is the computation. The connectome result is an existence proof, at fruit-fly scale, +of the principle the engine is built on. I backed the parallel with proofs on real data (Brown corpus). + +**Proof 1 — structure is the computation, with no training.** Build a classifier by *structuring* +real documents: encode each, bundle them per category into a prototype, classify held-out documents by +nearest prototype. No gradients, no training loop. Held-out accuracy: + +> **0.76** correct vs **0.17** chance (6 classes). + +The bundled prototypes *are* the classifier. That is the engine's analog of wiring driving behavior — +the structure does the work the moment it exists. + +**Proof 2 — learning is structural reorganization (the honest version).** It is tempting to claim the +representation "collapses to low rank as it learns." It does not, naively: the **raw document cloud's +effective rank grows** with more samples (8.9 → 20 → 32 → 45 as docs/class go 2 → 20). Accumulation is +*not* learning. What *is* learning is the reorganization that follows: the **task** structure is +low-rank, and consolidation (SVD — the engine's existing consolidation faculty) finds it. + +| consolidate prototypes to rank | held-out accuracy | +|---|---| +| 6 (full) | **0.76** (lossless) | +| 4 | 0.70 | +| 2 | 0.43 (broken) | + +So the six categories live in a ~4–6 dimensional subspace even though the raw vectors span 45+. Learning +is the move that **separates the low-rank task structure (kept) from high-rank sample noise (discarded)** +— reorganizing the representation onto the subspace that carries the decision. That is precisely the +holostuff analog of a connectome being a specific, low-complexity wiring that holds the behavior: the +structure that matters is far smaller than the raw activity, and the work is in finding it, not in +piling up examples. + +### The honest boundary (kept) +The fly result is not "a brain was uploaded." The connectome models have no plasticity, coarse sensing, +and some embodied variants trained controllers *on top of* the wiring. The clean, defensible claim — the +one holostuff shares — is narrower and solid: **a fixed structure, with no gradient training, can carry +real computation, and a good structure beats a random one.** holostuff measures both halves of that. + +--- + +## The single thread + +A representation is not just *what* it stores. Its **shape** decides what it can say (a circle cannot +represent an orientation or a sign flip; a Möbius strip can), and its **structure** — not a trained +weight matrix — can carry the computation outright (bundled prototypes classify; a connectome walks). +Learning, in this view, is choosing the right topology and reorganizing onto the right low-rank +structure — both of which holostuff now does, and measures. diff --git a/NOTES_concepts.md b/NOTES_concepts.md index 345de6b7..0200e7c6 100644 --- a/NOTES_concepts.md +++ b/NOTES_concepts.md @@ -3120,3 +3120,18158 @@ test_holographic_brain. (Noted from the previous round, now FIXED by this: the "consolidated value() rejects a raw probe" gap was this same bug.) Net: the creature was already optimized with the latest tech; the audit's deliverable is the confirming measurement, one robustness fix, and one honest negative. + +## PORTING UPSTREAM FROM A SIBLING PROJECT (TuneFM) -- verified, then adopted/adapted + +A sibling project that adopted the holostuff methodology on market data proposed five +improvements to send back upstream "because they help every application, not just trading." +Treated the document the holostuff way: VERIFIED every claim against the actual source (all +five were accurate about what holostuff did/didn't have), then let MEASUREMENT on this +substrate decide what earns its place. All five adopted, two adapted from the proposal. + +1. rfft bind + bind_batch/bind_fixed. The core bind used the COMPLEX fft; atoms are real, so + the REAL fft is exact (measured equal to ~7e-17) and ~1.5x cheaper -- switched it, and the + full algebra suite + the rescue_cracks canary pass, so every caller gets 1.5x for free. + Added bind_batch/bind_fixed (vectorised): ~2x even at 3 fillers, 5.5x at 64. Wired into + RecordEncoder.encode (verified bit-identical to the per-field loop). The new primitives are + in real use, not siloed. + +2. ScalarEncoder RBF kernel + kernel_at. Default stays sinc (uniform phases). kernel="rbf" + mints Gaussian phases -> a non-negative monotone RBF kernel; kernel_at(dx) returns the + similarity the encoder analytically realises (Bochner). VERIFIED the encoder IS its kernel + (measured cosine matches kernel_at to 0.001) and MEASURED the real win: recovering a + bimodal density within the range, RBF gets corr 0.73 and resolves both modes where sinc -- + one lobe over the range by construction -- gets 0.40 and sees one. (Did NOT find the + negative-lobe density corruption the document claimed in my configs, so did not claim it; + the mode-resolution win is the measured justification.) + +3. holographic_honesty.py -- the ablation ethos as a callable instrument. walk_forward_recall + (six checks a recall predictor must survive: beat chance, beat the persistence baseline, + collapse under a shuffle, magnitude correlation, net of cost) and bh_fdr (Benjamini- + Hochberg/Yekutieli false-discovery control -- the real gap: grep for benjamini returned + nothing, and a library that generates many candidates needs FDR). The module passes its + OWN audit: a planted edge clears chance with its shuffle collapsing, pure noise does not, + bh_fdr rejects 5 planted discoveries and spares 200 nulls. (Did NOT rewire the known-flaky + market test onto it -- left available for adoption rather than destabilise that test.) + +4. HoloForest.recall(with_agreement=True). The trees are independently seeded, so their + agreement is a free abstention signal. Returns (best, agreement=fraction of trees whose + own pick equals the forest's). Stored item -> 1.00, random query -> 0.59, so it separates + known from unknown. Guarded so the DEFAULT path is byte-identical (verified, incl. cosine- + tie order) and not slowed (per-tree work only when the flag is set). + +5. HolographicArchive.verify(). ADAPTED from the document's sketch, which assumed a bucket-of- + members API this archive does not have (it is WHT disjoint-slot image superposition). The + real, checkable version: reconstruct the most collision-prone stored images and confirm + each recalls back to its OWN index -- the disjoint-slot orthonormal-key guarantee, checked + on this build rather than assumed, using only the archive's own API. 6/6 exact clean and + after 4-bit plate quantisation; catches identity loss if quantisation ever corrupts it. + +Deliberately NOT taken (the document was honest about provenance and so is this): the WHT +plate memory, median-split trees, and resonator peeling were re-derivations on their side of +things holostuff already had. Net: cross-project port, every claim verified against source, +every change measured before adoption, two adapted to the real APIs, all backward-compatible. + +## VENDORING REAL MARKET + ON-CHAIN DATA FROM A SIBLING PROJECT (not just code, data) + +The sibling project (TuneFM-SOL) that produced the upstream-port document also shipped its +real datasets and some app/infra code. Brief: add its market and trading datasets alongside +the existing images/text/records, and extract anything else useful. Treated it the holostuff +way -- vendor REAL data in a lean, native form, wire it into the existing machinery, and be +explicit about what was deliberately left out. + +WHAT WAS VENDORED (real, checked-in, lean): + * data/sol_market.npz (0.60 MB) -- real SOL/USDT bars from Binance, multi-timeframe (5m/1h/1d) + plus BTC/ETH 1d cross-assets, each [time,open,high,low,close,volume,taker_buy,ofi] (taker_buy + = aggressive buy volume, ofi = order-flow-imbalance sign -- microstructure the permutation + tests and CandleCoder can chew on), plus a funding-rate [time,rate] series. Built from the + richest source file (the 62 MB -big export) but capped to recent bars and float32, so it is + 0.60 MB, not 62. load_sol_market(timeframe=...) in holographic_market.py; feeds the EXISTING + CandleCoder once prices are normalized to a working level (the coder lives in bp-space around + 1.0 -- a real contract detail, documented in the test: SOL round-trips to 0.13% normalized). + * data/onchain_traders.json (122 KB) -- realized on-chain Jupiter Perpetuals trades the sibling + read off Solana's public ledger: 58 wallet profiles + 869 realized trades. HONEST by + construction: every profile carries trade COUNT and a per-trade edge t-stat beside its PnL, so + a wallet green on a handful of trades reads as luck, not skill (the same n-problem the engine + keeps flagging). load_onchain_traders() in holographic_market.py. + +WIRED IN (reachable, not siloed): + * load_onchain_world() in unified_app.py turns the wallets into role-bound RECORDS (win_rate, + leverage, hold, bias, blowups bucketed to categoricals) LABELLED by honest skill: "skilled" + only when edge_t_stat >= 2 AND positive per-trade net, "burned" if liquidated and negative, + else "neutral" (distribution 4/37/17). Registered as the "onchain" dataset in DATASETS, so it + appears in the app's dataset picker next to world/reuters/brown/... Measured: the records are + genuinely learnable -- absorb a 70/30 split, held-out classify accuracy 0.95 (vs 0.33 chance + for the 3 classes). A tour line demonstrates both the SOL candles and the onchain records. + +DELIBERATELY NOT VENDORED (and why -- this matters as much as what was taken): + * onchain.py -- a live Solana/Helius RPC fetcher (hand-rolled borsh event decoder). It is + network-dependent infrastructure that does not fit holostuff's offline, minimal-dependency + philosophy, and the sandbox blocks Solana RPC anyway. Its OUTPUT (the trader JSON) is the + useful artifact; the fetcher is not. Its honest caveat is preserved in spirit in the labeling + above ("up over a window is survivorship/variance until proven; report count + per-trade edge + t-stat, treat green-on-12-trades as luck"). + * app.py (734 KB FastAPI app) and index.html (407 KB) -- the sibling's whole web app. Far too + large and infra-specific; holostuff has its own app surface. Not vendored. + * The 62 MB / 12 MB / 9 MB raw soldata exports, the quiz CSVs, TEAM_REGISTRY.md -- raw or + project-specific; the useful market signal was distilled into the 0.60 MB npz instead. + +The sibling's README is itself a substantial honest-measurement document (same methodology, +including holographic experiments); it was read for provenance but not vendored wholesale. +Net: two real datasets added in lean native form, wired into the existing CandleCoder, dataset +registry, tour, and tests; honest labeling baked in; clear boundary on what infra was left out. + +## FILESYSTEM: the recovery zip was silently dropping data/*.npz + +While vendoring sol_market.npz, found the close-out zip's exclude list carried a blanket +-x "*.npz", meant to skip transient model snapshots -- but it ALSO dropped the checked-in +datasets data/sol_5min.npz and data/sol_market.npz from the recovery zip. Since this tree is +not a git repo, the zip is the ONLY recovery path, so the tick dataset had quietly been at risk +the whole time. Fixed by removing the blanket *.npz exclude from the close-out rebuild so +data/*.npz is kept; transient snapshots are written to /tmp or root scratch (handled by the +"_"-scratch and /tmp conventions) rather than under data/, so they are still not shipped. + +## WHERE THE RECENT IMPROVEMENTS REACH ELSEWHERE -- an audit, with one unlock and one kept negative + +After the upstream port, traced each improvement to see where else it applies or what it unlocks. +Measured every candidate; applied what earned its place, kept the negatives. + +THE UNLOCK -- bh_fdr controls the project's own ablation table. The flagship "is VSA +load-bearing?" table (holographic_ablate.py) scans ~6 subsystems and decides each verdict on +that subsystem's OWN 95% CI. But a table is a SCAN, and scanning enough subsystems means one can +clear a per-test bar by luck -- exactly the exposure the honesty module's bh_fdr exists for. Added +fdr_verdicts(): each subsystem gets a PAIRED PERMUTATION p-value (holo vs baseline; seeds are +shared so scores pair by seed -- a sign-flip test, enumerated exactly for the handful of seeds; +falls back to a two-sample label-permutation test when an arm ran fewer seeds), then bh_fdr +(Benjamini-Yekutieli, dependent=True since the subsystems share data/methodology) holds the +false-discovery rate among the surviving "load-bearing" calls across the WHOLE family. On the real +table both load-bearing verdicts (topic-classify, noisy key->value) are unanimous 6-seed wins +(p=0.0156) and SURVIVE the family-wise bar. HONEST PROPERTY found while testing: BY is conservative +-- a single unanimous 6-seed win (finest p reachable = 1/64) does NOT survive BY across a 6-test +family on its own (top-rank threshold ~0.007); the two real verdicts survive because they SHARE the +win (the rank-2 threshold is more lenient). So 6 seeds is near the floor for clearing BY-FDR; more +seeds tighten it. Wired into the ablation _demo (p + FDR columns) and the tour. Pinned in +test_ablations. This makes the engine's core epistemic instrument rigorous against multiple-testing. + +APPLIED (consistent, behaviour-safe) -- bind_batch in KnowledgeStore.add. The relations record +builder used the same bundle([bind(role,filler) for ...]) pattern already vectorised in +RecordEncoder; switched it to one batched FFT. Identical to the loop at 1e-12, relations recall is +exact-key (wide margin, robust to the ~1e-16 batched-vs-scalar difference), all relations tests +pass. A small, consistent application -- faster as records widen, no behaviour change. + +KEPT NEGATIVE -- bind_batch in the creature encoder. CreatureEncoder.encode binds role->value for +every sense each step (a hot path: encode ~169us actually costs MORE than decide ~80us). bind_batch +measured 1.38x there and identical to the loop at 1e-12 -- BUT batched and scalar FFT differ at +~1e-16, and that is enough to flip a knife-edge tie-break in the starved-maze rescue trajectory: +the rescue_cracks CANARY FAILED. Reverted. The creature's deterministic reproducibility outweighs a +per-step 1.4x. Note the asymmetry with RecordEncoder (which tolerated the identical change): record +classification is wide-margin argmax, the maze rescue is tie-sensitive -- the SAME 1e-16 perturbation +is harmless in one consumer and trajectory-changing in the other. This is why the canary gates +compute-path changes and bit-exactness matters for the creature specifically. + +ASSESSED, NOT FORCED (no high-value internal consumer right now, so left as available capability): + * HoloForest with_agreement (abstention signal) -- the forest is used for scale benchmarks and + the recall-index ablation, not for any decision that should abstain; the unified mind's + classify/cleanup run on vectorised prototype matrices, not the forest. Wiring agreement somewhere + just to use it would be decorative. Kept as an available signal for callers. + * ScalarEncoder RBF / kernel_at (non-negative density kernel) -- no existing path reads a scalar + bundle as a density where sinc's lobes bite; forcing RBF into CandleCoder (whose flaky test and + bp-space contract argue against churn) would be fishing for a win. Available, not forced. + * archive verify() pattern -> other memories -- does NOT transfer: HolographicMemory/ + PartitionedMemory are intentionally LOSSY (finite capacity is a measured feature, already + instrumented by capacity_curve/recall_all), so "verify exact recall" is the wrong check for them. + The archive's verify() is specific to an exact-recall store with stored ground truth. + +Net: one genuine unlock (FDR over the ablation family), one consistent safe application, one kept +negative with a sharp lesson about why bit-exactness matters more in the creature than elsewhere, +and three capabilities honestly left unforced rather than wired in decoratively. + +## ADVISORY-PANEL DESIGN REVIEW -- sixteen lenses, debated to one build + three queued + +Ran the cross-disciplinary panel as a design review: each seat proposed one change grounded in +its field's real published method, then the proposals were clustered, cross-examined against +holostuff's constraints, and measured before belief. (Attributed to seats/methods, not to the +individuals as personal opinion.) Two proposals were measured during the debate to keep the +convergence evidence-based: + * Duda/ANS: the int8 stream carries ~6.85 bits/symbol of entropy vs 8 bits stored -> ANS would + save ~14% losslessly on top of int8. Real but modest; a bit-exact NumPy coder is fiddly. QUEUED. + * Tarter/Cranmer null-calibration: the random-query null is already well-behaved; a clean match + sits far above it. Cheap, low-risk, broadly useful. BUILT. + +BUILT -- RecallNull (holographic_honesty.py): turns a recall/cleanup similarity into an HONEST +false-alarm probability. fit() draws random queries against a codebook and records the best-match +cosine each reaches (the empirical noise floor); pvalue(score) = fraction of that null reaching +score or higher = the chance noise alone would look this good. calibrated_recall(query, codebook) +returns (idx, score, p). MEASURED: a clean stored atom -> p~0; random queries are well-calibrated +(P(p<=0.05)=0.043, P(p<=0.20)=0.201, so a p<=alpha gate has false-alarm rate ~alpha); it tracks the +capacity cliff per recall (recalling pair-0 from a filling key->value trace, score 0.71->0.15 but p +stays ~0 because it is still above the noise floor -- the calibration CONFIRMS each recall is real); +and it refuses to over-claim when a signal is genuinely swamped (p rises toward 1). Complements the +two existing abstention signals: HoloForest cross-tree AGREEMENT (structural) + RecallNull FALSE- +ALARM PROBABILITY (statistical). Pinned in test_holographic_honesty, demoed in the tour. + +QUEUED with evidence (in priority order, for a future round): + 1. Tero (2007) flow-conductance Physarum solver (Adamatzky seat) -- tubes thicken with Poiseuille + flux; a genuinely different algorithm from the current elitist-ant pheromone. Bar: beat + elitist-ant on the braided maze at equal cost. + 2. ANS entropy-coded save level (Duda seat) -- measured ~14% lossless on int8, if a bit-exact + NumPy coder round-trips cleanly. + 3. L1 / compressed-sensing archive recovery (Ozcan seat) -- behind a measurement: does it beat the + archive's CG least-squares past ~60% plate erasure? + +PARKED with rationale: sparse thinning / exact-inverse unbind / XPBD cleanup (overlap FHRR + +resonator, small expected gain, hot-path risk); SAH tree split (the ablation already shows median +split is not the bottleneck -- "scale win, not accuracy"); tensor-train codebook + Stam Helmholtz +binding (research-grade); SDF / quality-diversity (peripheral to the core algebra, fine as app +extensions). The panel's real output was the DEBATE cutting 15 proposals to 1 build + 3 evidenced +queue items + honest parks -- the engine's own method applied to its own roadmap. + +## DENOISING & GAUSSIAN SPLATS -- the measured cluster shipped (panel addendum II) + +Built the four measured breakthroughs that share one engine ("one operation seen several ways": +a denoiser is a map of the manifold signals live on, and holostuff already owned those maps). +All additive, opt-in, backward-compatible, pure NumPy, deterministic. + +B1 -- holographic_hopfield.py: modern continuous Hopfield cleanup (Ramsauer 2020 / Krotov & +Hopfield 2016 / Demircigil 2017). dense_cleanup(q, codebook, beta, steps) = z<-V^T softmax(beta Vq) +iterated; HopfieldCleanup.fit/cleanup/denoise. KEPT NEGATIVE: ties one-shot NN on IDENTITY (NN +already optimal; at beta->inf it REPRODUCES the hard decision exactly -> backward compatible). +The real win is CONTINUOUS-VECTOR DENOISING: a recovered vector at cosine 0.45 cleans to ~1.0 +(measured, mean over trials = 1.000 across dim/noise). A single high-noise draw can occasionally +miss; the mean is what we ship (test averages over trials). + +B10 -- generate() in holographic_hopfield.py: iterate the cleanup from PURE NOISE with annealed +beta-up / noise-down = a tiny holographic diffusion. Measured: nearest-pattern cosine 0.5->1.0 in +~8-12 steps; generation and denoising are the SAME operation in different regimes. KEPT NEGATIVE: +over a BARE codebook this returns stored atoms (degenerate sampler) -- the interesting regime is a +COMPOSED/continuous manifold. + +B8 -- holographic_splat.py: a splat scene IS a superposition (bundle). splat_fit (matching pursuit +with isotropic Gaussian atoms) / splat_render / splat_denoise. MEASURED on a real (log-return, +log-volume) SOL density: ~20 superposed Gaussians -> ~31 dB at ~3.5% of pixels; fitting few splats +to NOISY data denoises it (+~5 dB to clean, no capacity for noise). BRIDGE pinned in test: the RBF +ScalarEncoder's similarity profile is a Gaussian bump (peaks at the encoded value) = Gaussian +splatting in the hypervector domain. SCOPE/kept-negative: isotropic matching pursuit only; +anisotropic covariances + gradient refinement (full 3DGS) deliberately out of scope. + +B7 -- holographic_denoise.py: denoising as MANIFOLD PROJECTION (Milanfar: a denoiser is a map of +the signal manifold; consolidation IS that map) + the Plug-and-Play/RED loop (Venkatakrishnan 2013; +Romano-Elad-Milanfar 2017). fit_manifold (SVD = consolidation) / manifold_denoise (project) / +codebook_denoise (re-exports dense_cleanup) / pnp_restore (data-fidelity <-> denoise, any denoiser). +MEASURED on real SOL price windows: projection denoising WINS as noise grows (+3.85 dB at sigma=0.8) +but HURTS at low noise (-1.4 dB over-smoothing -- the Donoho/Milanfar threshold-selection problem, +KEPT NEGATIVE) and DESTROYS random no-manifold data (-5 dB, honest control, pinned in test). pnp +inpainting test: restoration beats the masked measurement. + +Tests: test_holographic_hopfield.py (3), test_holographic_splat.py (3), test_holographic_denoise.py +(3) = +9 (569 -> 578). Tour block added (denoise+splats line). Wired as standalone opt-in modules; +no change to bind/value/decide/cleanup-defaults -> creature tie-sensitive path untouched (canary not +required). STILL QUEUED (each behind its measurement bar, next passes): B2 sparse block codes + +scaled resonator; B3 SPRT streaming recall; B4 propagator binding (needs a learnable-dynamics +signal); B5 rate-distortion ANS save level (bit-exact coder is the fiddly part); B6 Tero flow / +fragment assembly; B9 non-local-means via content-addressable recall (bar: beat manifold projection +on textured/non-low-rank signals). + +## B9 -- NON-LOCAL-MEANS DENOISING VIA CONTENT-ADDRESSABLE RECALL (shipped) + +Built B9 from the queue. "Find the patches that look like this one and average them" (Buades-Coll- +Morel NLM 2005; BM3D Dabov 2007) IS content-addressable recall -- so it runs on holostuff's own +index. Added HoloForest.recall_k(query, k, beam) -> (indices, cosines) ranked over the same unioned +candidate set recall() uses (stays SUB-LINEAR; default recall() untouched, byte-identical). Added +nlm_denoise(patches, k, h, use_forest) in holographic_denoise.py: per patch, recall its k nearest +(forest sub-linear, or exact cosine fallback for small sets / determinism), softmax(cosine/h) weight, +average -- cancels iid noise across near-duplicates (~1/sqrt(k)). + +MEASURED (real SOL motif-windows, M motifs x R=8 repeats + noise): NLM-via-forest 11.7 dB vs rank-8 +projection 7.3 dB vs raw 4.6 dB -- and the sub-linear forest path (11.67) matches exact kNN (11.77). +COMPLEMENTARITY confirmed and pinned as a KEPT NEGATIVE test: on low-rank-but-NOT-self-similar data +(every patch unique), projection WINS (2.8 dB) and NLM has nothing to average (0.5 dB). So B7 +(manifold projection) and B9 (NLM) cover DIFFERENT structure: low-rank-not-similar vs +self-similar-not-low-rank. Tests: +3 in test_holographic_denoise.py (nlm beats projection on +self-similar; projection beats nlm without self-similarity; recall_k finds near-duplicates) -> +578 -> 581. Tour line added. recall_k is a pure addition; default forest recall path unchanged. + +REMAINING QUEUE (each behind its bar): B3 SPRT streaming recall (builds on RecallNull; clean +optimality bar); B2 sparse block codes + scaled resonator; B5 rate-distortion ANS save level (fiddly +bit-exact coder); B4 propagator binding (mechanism real, prediction an honest near-negative on +markets -- ship as content-addressable-trajectory capability); B6 Tero flow / fragment assembly. + +## B3 -- SPRT STREAMING RECALL (shipped): sample-optimal sequential detection + +Built B3 from the queue. RecallNull turns ONE recall into a calibrated false-alarm probability; +SPRTRecall (holographic_honesty.py) turns a STREAM of cues for the same hypothesis into a Wald +sequential test: accumulate the per-cue log-LR log(p(score|match)/p(score|null)) and decide the +moment it crosses a Wald boundary A=log((1-beta)/alpha) / B=log(beta/(1-alpha)). RecallNull's noise +floor IS p(score|null); match density is fit from genuine noisy-target recalls. API: SPRTRecall( +null_scores, match_scores, alpha, beta).reset()/.update(score)->MATCH|REJECT|CONTINUE/.decide(stream, +cap)->(decision, n). Gaussian-fit densities. + +MEASURED on real recall scores (the optimality bar): across the overlapping regime SPRT reaches a +target (alpha,beta) error pair in ~HALF the samples of the best fixed-N rule -- e.g. avg 2.8 cues vs +fixed-N 6 at ~2% error (also 1.7 vs 3, and 4.8 vs 9 at heavier overlap). Wald optimality confirmed. +KEPT NEGATIVE / boundary: when the cue carries NO per-sample information (signal fully swamped, match +and null distributions identical -- e.g. sigma=9 noise), neither SPRT nor fixed-N can decide and SPRT +just hits the cap; streaming only helps when each cue carries SOME evidence. Tests: +2 in +test_holographic_honesty.py (clear streams decide MATCH/REJECT; SPRT uses fewer samples than fixed-N +at matched error) -> 581 -> 583. Tour line added (sequential recall). Pure addition; nothing else +touched. + +REMAINING QUEUE: B2 sparse block codes + scaled resonator; B5 rate-distortion ANS save level (fiddly +bit-exact coder); B4 propagator binding (mechanism real, prediction honest near-negative on markets +-- ship as content-addressable-trajectory capability); B6 Tero flow / fragment assembly. + +## B4 -- PROPAGATOR BINDING (shipped): dynamics as an algebra of binds + +Built B4 from the queue. holographic_dynamics.py / Propagator: learn a fixed bind operator U so +state(t+1) ~ bind(U, state(t)). In HRR's Fourier domain bind is elementwise multiply, so the learned +operator is a per-frequency least-squares transfer H[k]=sum X1 conj(X0)/sum|X0|^2 (Koopman-in-Fourier +/ DMD / the same FFT-on-a-torus Stam and Puckette use). step(state)=bind(U,state) LITERALLY (pinned +in a test) -> prediction is one bind; recall_at(state,k) applies a Wiener-regularised inverse operator +k times -> the trajectory is CONTENT-ADDRESSABLE. + +MEASURED (honest, complete picture): + * POSITIVE CONTROL (dynamics that ARE a bind): next-state prediction cosine 0.997 vs persistence + 0.528 -- when dynamics are bind-shaped the propagator recovers the operator and predicts full + states. This is the method's honest SCOPE. + * DURABLE WIN: content-addressable round-trip (forward k / back k) cosine ~0.9995 -- past states + recoverable regardless of predictability. + * KEPT NEGATIVE (real SOL returns, scalar next-return): propagator RMSE 0.0088 vs mean 0.0063 -- + ties/loses to mean; near-efficient-market returns have no linear structure. Also a structural + kept-negative: the bind operator is a CIRCULAR convolution, so as a next-VALUE predictor on a + shifted signal it suffers wrap-around and an unconstrained full operator would do better -- the + bind framing buys the exact content-addressable round-trip, not best-in-class scalar prediction. + * Inverse uses conj(H)/(|H|^2+eps) (Wiener-regularised) -- the Plate tradeoff made explicit (exact + deconvolution is precise but amplifies near-null frequencies; regularised is robust). + +Tests: test_holographic_dynamics.py (+4: step IS a bind; predicts bind-shaped dynamics; trajectory +content-addressable; rollout shape) -> 583 -> 587. Tour line added. Pure new module, nothing else +touched. + +REMAINING QUEUE: B2 sparse block codes + scaled resonator; B5 rate-distortion ANS save level (fiddly +bit-exact coder); B6 Tero flow / fragment assembly. (B1,B3,B4,B7,B8,B9,B10 shipped.) + +## MÖBIUS / NON-ORIENTABLE TOPOLOGY (shipped): matching representation topology to data + +Prompted by a question -- circles, sign flips, and noise recur throughout the engine; would a Mobius +strip define some things better than a circle? Searched the literature (neural population activity +traces a manifold whose TOPOLOGY MATCHES the variable: ring for head-direction, torus for grid cells, +Klein bottle / Mobius for ORIENTATION). holostuff binds by circular convolution, so its native shape +is the circle/torus -- right for a directed angle, WRONG for two cases: + + * AXIAL data (theta == theta+pi: orientation, director/nematic fields, phase-mod-pi). On a circle + theta and theta+pi are ANTIPODAL (sim -1) though they are identical. Correct base = projective + line RP^1 = the Mobius double-cover's base. Fix = double-angle map theta -> 2*theta. + * SIGN-FLIPPING data f(t+T) = -f(t) (antiperiodic, a Mobius double-cover in time): all energy in + ODD harmonics; the periodic/circular basis is blind to it. + +Built holographic_mobius.py: AxialEncoder (double-angle phasor encoder, theta and theta+pi map to the +SAME hypervector), antiperiodic_fraction / antiperiodic_split (diagnose + extract the sign-flipping +component). + +MEASURED: + * axial recovery error (values reported as theta OR theta+pi at random): naive circle 0.470 rad vs + Mobius double-angle 0.002 rad; sim(theta,theta+pi) naive -0.22 vs Mobius +1.00. + * sign-flipping signal: ~100% of energy antiperiodic (periodic component ~1e-14). + +KEPT NEGATIVE / SCOPE: use ONLY for genuinely axial or sign-flipping data -- on DIRECTED data the +circle is correct and the double-angle encoder WRONGLY merges theta with theta+pi (it discards the +half-turn on purpose). Also NAMES an old kept negative: binary quantization maps to +-1, itself a +Z2/antipodal (Mobius) identification -- exactly why it distorted circular geometry, and exactly why it +would be right for axial/sign-flip data. Same lesson: topology must match the data. + +Tests: test_holographic_mobius.py (+6: axial identifies theta==theta+pi; naive circle disagrees; +recovers orientation despite pi-flips; merges half-turn on purpose [scope]; antiperiodic fraction +detects sign flip; split reconstructs) -> 587 -> 593. Tour block added. Pure new module. + +## STRUCTURE-FIRST COMPUTATION + REORGANIZATION (measured, no module): the fruit-fly-connectome parallel + +Prompted by the embodied fly-connectome result (Shiu et al. Nature 2024 wired a leaky-integrate-and- +fire model STRAIGHT FROM the FlyWire connectome, no training, ~95% sensorimotor accuracy; Eon 2026 +drove a physics fly from the wiring alone). Load-bearing claim across the coverage: STRUCTURE CARRIES +COMPUTATION (biological wiring beat random graphs / standard nets). That is holostuff's thesis. Backed +it with proofs on real Brown-corpus data: + * PROOF 1 (structure = computation, no training): bundled per-class prototypes (just bind+bundle, no + gradients) classify held-out documents at 0.76 vs 0.17 chance (6 classes). The structure IS the + classifier -- the engine's analog of wiring driving behavior. + * PROOF 2 (learning = structural REORGANIZATION, honest): the RAW document cloud's effective rank + GROWS with samples (8.9 -> 45) -- accumulation is NOT learning. But the TASK structure is low-rank: + consolidating the prototypes (SVD, = our consolidation faculty) to rank 6 preserves accuracy + exactly (0.76), rank 4 keeps most (0.70), rank 2 breaks it (0.43). Learning is the reorganization + onto the low-rank task subspace, separating it from high-rank sample noise -- consolidation is the + holostuff move that mirrors a connectome being a specific low-complexity wiring that holds behavior. +Written up with the Mobius findings in MOBIUS_AND_STRUCTURE.md (outputs). + +REMAINING QUEUE: B2 sparse block codes + scaled resonator; B5 rate-distortion ANS save level; B6 Tero +flow / fragment assembly. (B1,B3,B4,B7,B8,B9,B10 shipped; Mobius shipped.) + +## HOLOGRAPHIC MACHINE (shipped): inception -- a program encoded as a vector, executed by the substrate + +Prompted by an inception question: a hard drive has physical structure, data in that structure, and -- +executed -- an OS, a VM, an OS inside the VM. holostuff had the lower rungs (vector = platter, +derived_atom = format, role-filler + nested composition = file system); the missing rung is an OS that +EXECUTES. Built holographic_machine.py / HoloMachine: a program is encoded as ONE hypervector and run by +the engine's own bind/bundle/cleanup. Instructions and data share one vector space (von Neumann, +holographically). "Format the drive" = fix a seed (lays down roles OP/ARG/SLOT, opcode atoms, data atoms, +POS(i) addresses, all via derived_atom). Instruction set: LOAD/BIND/BUNDLE/PERMUTE/HALT. + instruction = bundle(bind(OP,opcode), bind(ARG,operand)); program = bundle_i bind(POS(i), instruction_i). +run() unbinds each address, CLEANS opcode+operand against codebooks (wide-margin, robust to crosstalk), +dispatches. Operands are cleaned to exact atoms before use, so ACC is EXACT despite noisy reads. + +MEASURED: + * Correctness: LOAD a; BIND b; BUNDLE c -> ACC == bundle(bind(a,b),c) cosine 1.0000; trace exact. + * DRIVE SIZE (capacity cliff, KEPT NEGATIVE): instruction-decode ~100% up to a length that scales with + dim -- ~32 instructions reliable at dim 1024, ~128 at dim 4096 -- then bundling crosstalk overwhelms + cleanup. Capacity is finite; the cliff is the honest HRR wall. + * INCEPTION DEPTH (the law): a program nested as the ONLY file at each level survives 8+ levels deep + (a pure unitary bind chain barely degrades); a program buried among OTHER files on each disk corrupts + after ~3-4 levels. Depth is set by clutter per level -- nest as deep as you like if each level is + clean, only a few levels on a busy disk. Both scale with dim. + +The stack: platter (vector) -> format (derived_atom) -> file system (bind/bundle/compose_nested) -> OS +(HoloMachine.run) -> VM-in-OS (nest a program inside a disk inside a disk). Written up in +HOLOGRAPHIC_INCEPTION.md (outputs). + +Tests: test_holographic_machine.py (+6: executes exactly; HALT stops; PERMUTE; 32-instr decodes fully; +clean nesting deep; busy-disk depth floor [kept negative]) -> 593 -> 599. Tour block added. Pure new module. + +REMAINING QUEUE: B2 sparse block codes + scaled resonator; B5 rate-distortion ANS save level; B6 Tero +flow / fragment assembly. (B1,B3,B4,B7,B8,B9,B10 shipped; Mobius shipped; HoloMachine shipped.) + +## HOLOGRAPHIC FUNCTIONS + CALL (shipped): functions embedded and executed in the holographic space + +Prompted by an inception follow-up: can we embed and execute FUNCTIONS within the holographic space +(not as Python files)? Folders/partitions to reduce confusion? What does it enable, including things +we didn't plan for? Measured all of it on the substrate, then shipped the load-bearing piece. + +MEASURED (real numbers): + * A function you DEMONSTRATE instead of write: a key->value (or input->output) mapping stored as ONE + vector M=bundle(bind(k_i,v_i)); apply f(k)=cleanup(unbind(M,k)). 100% to ~120 pairs at dim 4096, + cliff at ~240 (87%). This is HolographicMemory used as a learned, content-addressable function -- + no code written, only examples given. + * Functions in a holographic LIBRARY, called by name: define ACC->ACC sub-programs, bundle them into + ONE library vector, CALL by name -> the body is extracted (unbind) and run on the current ACC. + 'LOAD a; CALL tag_b; CALL shift' == permute(bind(a,b)) cosine 1.0. Functions compose like data. + * FOLDERS/PARTITIONS reduce confusion: at 256 items a flat HolographicMemory recalls 86%, a 16-folder + PartitionedMemory recalls 100% -- partitioning cuts crosstalk per query (folders already exist as a + primitive; this just names/measures the benefit). + * DIDN'T PLAN FOR: (a) behavioral content-addressing -- retrieve a function by an EXAMPLE of what it + does (a->permute(a) retrieves 'shift'); (b) function arithmetic -- bundle(f1,f2) is a function that + carries BOTH answers (0.18/0.18 symmetric), i.e. you can average programs like vectors. + +SHIPPED: holographic_machine.py extended -- OPCODES gains CALL; HoloMachine.define(name, program) embeds +a named ACC->ACC function into a single library vector; run() gains init_acc + CALL dispatch (extract by +name, run on current ACC, recursion-guarded). Backward compatible (init_acc defaults None; non-CALL +programs unchanged). The other capabilities use existing primitives (HolographicMemory = demonstrated +function; PartitionedMemory = folders), so they were measured/named, not re-implemented. + +WHY IT MATTERS (the multiplier): code and data now share one algebra, so EVERY engine faculty applies to +programs too -- consolidate (compress a program), denoise (clean a corrupted program), factorize a +program into parts, index programs for content-addressable retrieval, even generate new programs with the +B10 sampler. The honest boundary: this is not a fast general CPU (Python is faster); its edge is +deterministic, inspectable, composable, content-addressable code-as-data. Written up in +HOLOGRAPHIC_FUNCTIONS.md (outputs). + +Tests: test_holographic_machine.py (+4: CALL runs a library function; CALL composes; library is one +vector; run backward-compatible) -> 599 -> 603. Tour line added. + +REMAINING QUEUE: B2 sparse block codes; B5 rate-distortion ANS; B6 Tero flow. Also teed up: adaptive-rank +denoising (cash B7's low-noise kept negative). (B1,B3,B4,B7,B8,B9,B10 + Mobius + HoloMachine + CALL shipped.) + +## B5 -- RATE-DISTORTION GEOMETRY-PRESERVING CODE (shipped): KLT -> quantize -> rANS + +Built B5 from the queue. holographic_ratedistortion.py: spend the minimum bits that preserve the +DECISION GEOMETRY (cosines), not raw values, by chaining three pieces the engine half-owned -- the +classic transform-coding pipeline: + consolidate (KLT/SVD) -> uniform scalar quantize the coefficients -> rANS entropy code +Consolidation IS the KLT (decorrelates), so one quantization step on the coefficients is near +rate-distortion-optimal and the entropy coder spends bits proportional to each component's entropy +(water-filling for free). rANS (Duda's ANS) codes to the Shannon limit. + +THE FIDDLY PART, DONE FIRST (the gate): a pure-NumPy bit-exact static rANS coder. Verified 40/40 random +streams round-trip EXACTLY (the determinism rule depends on it), coding within ~0.3% of entropy vs +int8's flat 8 bits/sym. Only after the gate passed was anything wired to it. + +MEASURED (honest, complete): + * WIN on genuinely low-rank engine state (bundled sense states, energy fully at rank 16): matches + int8 fidelity (cosine 0.99998) at ~191 bits/vec vs int8's 2048 -- ~11x smaller than int8, ~43x vs + float32. At target 0.9999, ~7x. File format (save_rd/load_rd) measured 6.2x smaller than int8. + * KEPT NEGATIVE: on full-rank data (SOL RETURNS, ~rank 64/64) no subspace to exploit -> rd loses; + it auto-falls-back to int8 in the save path so it is never larger. Like B7, helps only where real + low-rank structure exists. + * METHODOLOGICAL NEGATIVE: participation-ratio "effective rank" misleads -- smooth SOL PRICE windows + looked rank ~4 but a heavy spectral tail needs rank ~40 for high cosine (rank-8 only reaches 0.93). + Judge low-rank-ness by energy concentration / truncation cosine, not the participation ratio. + +WIRED: holographic_core.save gains quant="rd" (beside int8/auto): for low-rank 2D float arrays it stores +the packed rd code (basis f32 + rANS bytes) and falls back to int8 where rd wouldn't beat it -- so it is +always at least as small as int8 and never breaks a save (SelfOrganizingMind round-trips, classifications +preserved). load reconstructs. Standalone save_rd/load_rd (.rdc) provided too. Bit-exact rANS keeps the +determinism guarantee. + +Tests: test_holographic_ratedistortion.py (+5: rANS bit-exact; rANS ~entropy; geometry code preserves +cosines; beats int8 on low-rank; kept-negative full-rank) + test_core_persistence.py (+2: rd save level +safe/non-breaking; rd activates+shrinks low-rank) -> 603 -> 610. Tour line added. + +REMAINING QUEUE: B2 sparse block codes + scaled resonator (the +5-orders-of-magnitude capacity lever, +freshly grounded -- Hersche/Langenegger); B6 Tero flow / fragment assembly. Also teed up: adaptive-rank +denoising (cash B7's low-noise kept negative). (B1,B3,B4,B5,B7,B8,B9,B10 + Mobius + HoloMachine + CALL shipped.) + +## HOLOGRAPHIC KAN (shipped): a deterministic Kolmogorov-Arnold readout on holostuff encoders + +A panel/user question -- KANs (Kolmogorov-Arnold Networks: a function as a SUM of learnable univariate +splines, F(x)=sum_j psi_j(x_j)) sounded related to our encoder/bundle work. Checked the literature (Liu +et al. 2024; learnable univariate functions on edges, nodes just sum; B-spline basis; adaptive grid; +interpretable; slower than MLP, curse-of-dimensionality on splines). It IS related, and we built the +connection deterministically. Two threads, one module holographic_kan.py: + + * THREAD 1 -- AdaptiveScalarEncoder: a ScalarEncoder whose grid ADAPTS to the data via a monotonic + empirical-CDF warp -- KAN's "move the spline knots to where the data is", fit once and frozen + (stays deterministic). basis(x) = similarities of encode(warp(x)) to grid anchors = the spline + basis (the RBF encoder's similarity profile is a Gaussian BUMP, exactly a B-spline-like basis). + * THREAD 2 -- HolographicKAN: a single-layer KAN. Each feature -> its encoder's basis activations -> + psi_j(x_j)=a_j . basis_j(x_j); prediction = SUM over features (the Kolmogorov-Arnold inner sum = + holostuff's bundle). Output is LINEAR in the coefficients a, so they are fit by ridge LEAST SQUARES + -- NO backprop. psi_j are recoverable/plottable (KAN's interpretability), all deterministic. + +So: a KAN whose splines are holostuff encoder bumps and whose training is a linear solve -- KAN's idea +in holostuff's idiom (deterministic, interpretable, structure-first). + +MEASURED: + * additive target f=sin(2pi x1)+4(x2-.5)^2: test R^2 0.999; recovered psi_1 vs sin corr 1.000, + psi_2 vs quadratic corr 1.000 (interpretable parts recovered); linear readout only R^2 0.54. + * adaptive grid beats uniform on a SKEWED feature (R^2 0.41 vs 0.25 -- resolution follows density); + on UNIFORM data the warp is ~identity, a kept tie (no help, no harm, costs a stored CDF). + * KEPT NEGATIVE: the single-layer additive form cannot represent feature INTERACTIONS (x1*x2 -> R^2 + ~0), while the additive control x1^2+x2^2 -> R^2 0.997. Boundary shown; needs a 2nd layer or + explicit interaction features. + +Relation kept honest: cousins not twins -- KAN learns its univariate functions by backprop and is a +neural-net approximator; ours fixes the encoder, learns only the linear readout, and is structure-first. +The shared heart (sum of univariate basis-bump functions = bundle of per-feature encodings) is real. + +Tests: test_holographic_kan.py (+6: additive fit+recovery; beats linear; adaptive>uniform on skewed; +kept-negative interactions; warp maps skewed->uniform; warp identity before fit) -> 610 -> 616. Tour +line added. Pure new module (encoders + cosine + least squares; no kernel/compute-path change). + +REMAINING B-QUEUE (on hold per user): B2 sparse block codes + scaled resonator; B6 Tero flow / fragment +assembly. Teed up: adaptive-rank denoising (B7 low-noise negative). (B1,B3,B4,B5,B7,B8,B9,B10 + Mobius + +HoloMachine + CALL + Holographic-KAN shipped.) + +## GENERATIVE COMPRESSOR, part 1: the recipe-store (shipped) + +Follows the "proven structure has no noise" debate. A structure BUILT by a deterministic proof carries no +noise, so it serialises to its generator losslessly: store the recipe, not the expanded vectors, and +replaying reproduces it BIT-EXACT. This is the easy, exact half -- when we are the builder we already hold +the proof, so there is no search and no residual. + +holographic_recipe.py -- StructureRecipe: a tiny replayable build-graph. Ops atom/bind/bundle/permute/ +normalize each produce one result from a seed + earlier results; you build THROUGH the recipe so you get +both the vectors and the generator. `raw` stores a literal vector verbatim -- the escape hatch for +non-constructed data (the measured/lossy regime; stored float32). save/load is JSON (readable) with raw +payloads as binary float32. + +MEASURED: a 2000x512 derived codebook (~4.1 MB) -> ~68 KB recipe (~60x), replay max abs error 0.0 +(bit-exact). Deep nested structure recovered exactly at depth 8 -- no capacity cliff, because the recipe +names its leaves and replays rather than reading them out of a bounded superposition. KEPT NEGATIVE: the +`raw` escape hatch -- non-constructed/random data has no short recipe, so ratio ~0.99x (no win, no harm); +the compression is exactly the constructed fraction (half/half -> ~2x). Constructed ops replay bit-exact; +raw payloads are float32 (the regime where a residual coder belongs). + +Tests: test_holographic_recipe.py (+5). 616 -> 621. + +## GENERATIVE COMPRESSOR, part 2: the decompose search (shipped) + +The hard half: find the construction behind FOREIGN data. holographic_symbolic.py -- MDL-gated symbolic +regression. Rather than enumerate EML/operator trees (combinatorial; EML eval is expensive -- the EML +debate's kept negative), search a dictionary of elementary basis functions (SINDy-style; Brunton-Proctor- +Kutz 2016) by deterministic greedy forward selection, and choose the model by Minimum Description Length: +total bits = model bits (#terms x [index + coefficient]) + residual bits (Gaussian coding cost). MDL is +the gate: a term is kept only if it shortens the code, so the law is the shortest program explaining the +data -- the parsimony that makes extrapolation valid. On noise it adds nothing (honest refusal). + +MEASURED: recovered 2*sin(1.5x)+0.5x from noisy data (2 terms of 17), seed ~70x smaller than the data, +extrapolation RMS 0.016. THE MDL GATE CURES OVERFITTING (solves the generative-compression debate's kept +negative): MDL extrapolation 0.016 vs un-gated max-fit 7.6e5 (explodes out of range). On pure noise MDL +keeps 0 terms -> just the mean -> refuses to manufacture a law (no free lunch, enforced). The recovered +Formula is a generative SEED -- the measured-regime analogue of a StructureRecipe: build 2 finds the +recipe, build 1 stores it, the residual is what a B5 rate-distortion coder takes. + +KEPT NEGATIVES: the dictionary bounds what is discoverable (a law outside the basis, or a rate off the +frequency grid, is not found); the MDL coefficient-cost is a knob not a law; this is the tractable proxy +for the full EML-tree search (the uniform single-operator tree remains the theoretical, far larger space). + +Tests: test_holographic_symbolic.py (+5). 621 -> 626. + +THE TWO HALVES TOGETHER = the generative compressor the panel mapped: decompose (build 2) -> seed -> +generate/store (build 1) -> residual coder (B5), with the MDL/RecallNull parsimony gate keeping +extrapolation honest. CONSTRUCTED data: exact, no search (build 1). MEASURED data: search + residual +(build 2 + B5). Two regimes, two tools, one pipeline. + +REMAINING B-QUEUE (still on hold per user): B2 sparse block codes + scaled resonator; B6 Tero flow / +fragment assembly. Also teed up: adaptive-rank denoising (B7 low-noise negative). + +## RECIPE-STORE macro op + one-call decompose pipeline (shipped) + +Two follow-ups closing loose ends before resuming the B-list. + +(1) MACRO/LOOP OP for the recipe-store. The straight-line recipe stored N explicit ops for a regular +structure (a 2000-atom codebook -> ~60x). Added a `repeat(count, template)` op: a parameterised iteration +captured as ONE op, with a declarative template (local refs; {i} substituted in atom names; permute shift +can be the loop index "i"); the iteration's output is its last template result, so repeat emits `count` +results. `atom_range(prefix, count, unitary)` is sugar over it. Refactored handles to absolute RESULT +indices (a counter `_n_results`) since a macro produces many results per op. MEASURED: the 2000-atom +codebook now collapses to a 96-byte recipe -> ~42,000x (was 60x), replay BIT-EXACT, save/load round-trips. +A positional sequence (bundle_i permute(item_i, i)) is a 201-byte recipe matching the manual build exactly. +The win: the recipe now compresses REGULAR structure to its rule, not just per-vector. + +(2) ONE-CALL DECOMPOSE PIPELINE. Gave the symbolic `Formula` the SAME seed interface as StructureRecipe +(to_recipe/from_recipe/save/load/recipe_bytes/compression_ratio) -- a Formula IS the measured-regime +recipe (it generates a scalar signal where StructureRecipe generates vectors). Added `compress_signal(x,y, +path=None)`: decompose -> seed in one call. MEASURED: foreign data -> 135-byte seed file that reloads and +regenerates (in-window RMS 0.005) + extrapolates (RMS 0.016); to_recipe round-trips exact; residual (B5's +job) reported. This closes the pipeline end-to-end and is a concrete step toward the integration review's +"unify the seed/structure representation" recommendation (both regimes now share one seed interface). + +Tests: +3 recipe (atom_range ratio+bit-exact; repeat template vs manual; macro save/load), +2 symbolic +(Formula save/load roundtrip; compress_signal end-to-end). 626 -> 631. Additive (no compute-path change). + +NOW RESUMING THE B-LIST. Standing queue: B2 (sparse block codes + scaled resonator), B6 (Tero flow / +fragment assembly), plus the integration-review additions B7 (typed holographic structure: recipe=EML-tree +=program=scene), B8 (denoised structure decoding -- push the inception cliff deeper), B9 (manifold-aware +decompose). Also teed up: adaptive-rank denoising (B7-original low-noise negative). + +## B2 -- SPARSE BLOCK CODES + SCALED RESONATOR (shipped) + +The long-queued capacity lever, and -- per the blend discussion -- the deconfounder a superposition search +needs. holographic_sbc.py. An SBC atom is B integers (one active position per block); dense form is the +one-hot expansion, D = B*L. bind = (a+b) mod L per block (block-local circular convolution of one-hots) -- +EXACT, lossless, where dense circular-convolution binding accumulates crosstalk. The resonator factors a +product into one atom per codebook by annealed alternating projection (soft superposition estimates; +deterministic annealing beta 0.5->12 to explore-then-commit; random init to break the symmetric trap; +restarts), and verifies itself with a hard CONFIDENCE check: do the recovered factors RECONSTRUCT the +product? validated=True <=> correct. + +WHY annealing+restarts: a fixed-temperature softmax collapses to spurious fixed points (measured: ~0.13 +accuracy); signed-linear cleanup stalls too; deterministic annealing + reconstruction-validated restarts +fixed it. + +MEASURED (head-to-head at fixed D=256, F=3): SBC beats dense at every alphabet with signal -- N=10 1.00 vs +0.90, N=25 0.25 vs 0.15, N=50 0.05 vs 0.00 (consistent, modest edge). The confidence check tracks +correctness EXACTLY (validated<=>correct, precision ~1.0); coverage drops with alphabet so it verifies or +abstains rather than guessing. KEPT NEGATIVES: absolute capacity modest (both collapse by N~100; more +blocks/restarts raise both); SBC is a PARALLEL representation requiring sparse-block-coded data (beside the +dense kernel, not inside it); exact reconstruction-validation makes it abstain under product corruption +(honest but conservative). + +Tests: test_holographic_sbc.py (+5: exact block bind/unbind; clean factorization; confidence=>correctness; +high coverage at N=10; abstains on corruption). 631 -> 636. New standalone module, no compute-path change. + +THREAD TO CIRCLE BACK TO: the resonator's verified-factorization is the deconfounder for the underexploited +"superposition-parallel candidate search" in the decompose pipeline (blend candidate sub-expressions, let +the resonator factor which are present, verify by reconstruction). That is the next step on the blend thread. + +B-LIST STANDING: B2 DONE. Remaining: B6 (Tero flow / fragment assembly), B7 (typed holographic structure), +B8 (denoised structure decoding), B9 (manifold-aware decompose); teed up: adaptive-rank denoising. + +## STRUCTURAL DECOMPOSE: the verified resonator as the inverse of build-1 (shipped, blend thread) + +Picking the blend thread back up with B2's resonator now in hand. The honest scoping: the resonator's +unique power is factoring a BOUND PRODUCT of unknowns -- and a bound product is DISSIMILAR to its factors, +so you cannot read them off naively (measured: per-factor readout of a product is chance). That is exactly +where the superposition-parallel + deconfounded + VERIFIED search earns its keep -- and it applies to +compositional STRUCTURE (scenes, recipes, trees), not flat numeric sums (where greedy/matching-pursuit +already deconfounds, so the resonator adds nothing there -- kept scope). + +holographic_sbc.py gains `decompose_structure(composed, codebooks, L)` -> {picks, factors, verified, +present}: recover the generating recipe of a composed structure via the verified resonator -- the +structural INVERSE of build-1's recipe-store (build 1: recipe->structure forward; this: structure->recipe +inverse). `sbc_identity` lets a factor be detected ABSENT, so you can blend candidates INCLUDING an +'absent' option and factor which are PRESENT -- the literal "blend candidate sub-expressions, factor which +are present" idea. + +MEASURED: naive per-factor readout of a product (2,5,8) gives (2,0,0) = chance; the resonator recovers +(2,5,8) verified. Presence detection: a structure missing its third factor -> present=[True,True,False], +verified. Recovered recipe reconstructs the structure exactly. Superposition-parallel: resolves 1 of N^F +combinations (1000) without enumerating them. KEPT NEGATIVES: applies to compositional/product structure +with known codebooks, NOT to numeric-signal decompose (greedy already deconfounds sums); capacity is the +resonator's (modest, from B2); aliasing -- if two factor-combos reconstruct the same product, verification +cannot distinguish them (rare). + +This closes the loop the integration review wanted: build-1 (store known structure as recipe, forward) and +this (decompose foreign structure to recipe, inverse), with build-2 the numeric-signal analogue. Three +decompose regimes now: numeric signal -> law (build 2, greedy/MDL); composed structure -> recipe (this, +verified resonator); known structure -> recipe (build 1, no search). + +Tests: +3 in test_holographic_sbc.py (recover+verify; detect absent factor; naive-fails-resonator-succeeds). +636 -> 639. Additive (extends holographic_sbc; no compute-path change). + +## DECOMPOSE: multiplicative mode via the log transform (shipped) + +From the prime-factorization discussion: the deep takeaway was that MULTIPLICATIVE structure becomes +ADDITIVE in the right basis (a logarithm to a prime basis turns x into +). The transferable nugget: our +greedy/MDL decompose only finds ADDITIVE (sum-of-terms) laws, but a multiplicative law a*x^p*exp(cx)*... +becomes additive under log y. So holographic_symbolic.py gains a multiplicative path: + * `_eval_atom` gains a `log` kind (log|x|); `log_dictionary()` = {log|x|, x, x^2, x^3} -- the log-images + of power and exp-of-polynomial factors. + * `Formula` gains `log_space`: generate() exponentiates, so the additive log-fit becomes a PRODUCT law. + * `symbolic_regress(..., multiplicative=True)` fits log(y) (requires y>0); term selection runs in log + space, resid_rms is reported in the ORIGINAL space (comparable across modes). + * `compress_signal(..., mode='additive'|'multiplicative'|'auto')`. auto switches to multiplicative only + if it is COMPETITIVE in-sample AND generalizes better on a held-out tail. + +MEASURED: recovered exp(0.707 + 1.51 log x + 0.29 x) = 2*x^1.5*exp(0.3x) -- the exact product law, extrap +relRMS 0.017, which the flat additive basis only approximates. auto-selection over 6 seeds: additive data +-> additive 6/6 (never a false positive), multiplicative data -> multiplicative 4/6 (else falls back to +additive, which still fits). KEPT NEGATIVES: needs y>0 (and x>0 for the log|x| power-law basis); +multiplicative mode FAILS on additive laws (log of a sum is not a sum -- measured resid 0.378 vs 0.031); +auto-selection between the two FAMILIES is genuinely hard when both fit in-sample, so the selector is a +conservative heuristic (never false-positive; ~4/6 true-positive), erring toward additive. + +Tests: +5 in test_holographic_symbolic.py (recovers product law; requires y>0; log_space roundtrip; +auto never false-positives on additive; multiplicative beats additive extrapolation). 639 -> 644. +Additive (extends the decompose; no kernel/compute-path change). + +## B7 KEYSTONE: one typed holographic structure (shipped) + +The integration review's headline was "substrate-integrated, orchestration-siloed": the engine's four +"structure" types -- a build RECIPE, an assembled PROGRAM (HoloMachine), an EML/expression TREE, and a +composed nested SCENE (UnifiedMind.compose_nested) -- are not four things. They are ONE directed graph of +the same primitives replayed to a vector, and StructureRecipe already IS that graph. holographic_typed.py +makes the unification concrete and MEASURED with adapters that reproduce each source bit-exactly: + * program_to_recipe(machine, program) == HoloMachine.assemble (cosine 1.000000, max|diff| 0.0) + * tree_to_recipe(dim, seed, tree) == encode_tree (direct) (cosine 1.000000, max|diff| 0.0) + * nested_scene_to_recipe(mind, groups) == mind.compose_nested (cosine 1.000000, max|diff| <1e-9) +The union alphabet across all three is just {atom, raw, bind, bundle, superpose} -- one small primitive +set, not five class hierarchies. A new `superpose` op (un-normalized sum) was added to StructureRecipe so +it can reproduce compose_nested's raw np.sum (the renormalizing `bundle` could not); backward-compatible. + +WIRED INTO UnifiedMind (the de-siloing): typed_structure() -> a fresh recipe at the mind's dim/seed; +realize(recipe) -> the single replay path; tree_structure(tree) and nested_scene_structure(groups) emit the +mind's own compositions AS typed structures (verified cosine 1.0 against the source ops). + +WHAT IS / ISN'T UNIFIED (kept honestly): + * Unified: the forward ENCODING. Name-addressable leaves (program opcodes, tree roles, scene group keys) + become atoms; rng-drawn leaves (a SceneCoder sub-scene) ride as `raw` payloads -- so the scene's + constructed STRUCTURE unifies into named atoms while its rng leaves stay raw (the constructed-vs-measured + split again; raw round-trips to float32 ~1e-8, constructed all-atom round-trips truly bit-exact). + * NOT unified: SEMANTICS (program execution / the CALL library, an EML node's scalar eval) -- those are + layers above the encoding; program_to_recipe rejects CALL as out of scope. And the INVERSE (decode a + foreign vector to a structure) is the resonator's job (decompose_structure), bounded by crosstalk -- a + structure here is a GENERATOR, not a parser. B8 (denoised decode) and B9 (manifold) target this one type. + +Tests: +7 in test_holographic_typed.py (program/tree/scene bit-exact; one-alphabet; CALL out of scope; +UnifiedMind wiring; superpose constructed round-trip). 644 -> 651. Additive: a new recipe primitive + +a new module + new UnifiedMind methods; no kernel/compute-path change. + +## B8: denoised structure decoding -- per-peel cleanup pushes the decode depth cliff (shipped) + +The B7 keystone unified the forward ENCODING; B8 attacks the INVERSE. A composed structure decoded by +ITERATED unbinding accumulates crosstalk, and -- the crux -- without cleanup that noise is carried into +the next query and COMPOUNDS. holographic_peel.py demonstrates this on a linked list +M = superpose_i bind(node_i, node_{i+1}) (itself a B7 typed structure via chain_recipe): + * MEASURED (16-node chain, dim 512): raw traversal (no cleanup) decodes ~2/15 hops then craters and + the carried vector diverges; per-peel cleanup decodes 15/15. ~2 -> full chain. Per-peel cleanup is + the whole game. + * Hard argmax cleanup and the B1 dense-Hopfield cleanup TIE on the discrete pointer (both 15/15) -- + exactly B1's kept negative: snapping to the nearest atom is Bayes-optimal for identity, so soft + cannot beat it there. + * The soft (Hopfield) update earns its keep on CONTINUOUS payloads: recovering off-grid scalar-encoded + values from a superposition, the soft blend beats hard snap-to-grid (cosine ~0.996 vs ~0.990) -- it + returns a mixture of nearby grid atoms, landing between grid points where the true value lives. + +KEPT NEGATIVES / scope: a commutative-bind chain has an INTRINSIC predecessor leak (unbinding node_i +surfaces node_{i-1} as a clean atom, since node_{i-1} bound it as its value and node_i*involution(node_i) +=delta). A forward traversal KNOWS its predecessor, so traverse() explains it away -- standard history-aware +decode, reported not hidden. Permuting the key/value does NOT fix it (permute distributes through the +convolution and the cancellation returns); disjoint key/value codebooks would, at the cost of chainability. +SBC block codes (B2) bind losslessly -> no leak and no cliff to push (so this is a DENSE-HRR technique). +The soft-vs-hard continuous win is modest (~0.6%). Reuses dense_cleanup (B1) and StructureRecipe (B7). + +Tests: +6 in test_holographic_peel.py (full-chain decode vs raw crater; hard/soft tie on pointers; correct +sequence; chain is a typed structure bit-exact; soft beats hard on continuous values; diverged hop marked). +651 -> 657. Additive: a new module; no kernel/compute-path change. + +## B9: manifold-aware decompose -- detect topology, decompose on the right manifold (shipped) + +The build-2 decompose assumes a flat line (a sum of elementary functions over an open interval). Many +signals live on a curved domain -- a RING (periodic), an antiperiodic MOBIUS band (only odd harmonics), +or a TORUS (two periods). On the wrong manifold a periodic signal needs many terms and EXTRAPOLATES BY +DIVERGING (a polynomial shoots off where the true signal repeats). holographic_manifold.py detects the +topology, then decomposes on the matched basis -- the decompose-side twin of the Mobius/AxialEncoder. + +DETECTION: detrend -> FFT for a candidate fundamental (the LOWEST significant peak; a strong harmonic is +not the fundamental) -> VALIDATE by how well a harmonic basis at that period actually fits (robust to FFT +leakage). Commensurate peaks -> periodic (ring; mobius if the odd-only fit is as good); an incommensurate +peak -> torus. A poor best-fit (R^2<0.9) is guarded back to "line" (no spurious rings). + +MEASURED: detected an OFF-GRID period (P~5, off the elementary freq grid) as ring; the matched harmonic +basis EXTRAPOLATES (RMS 0.024) where the flat-line polynomial DIVERGES/fails (RMS ~1.0 ring, ~5.4 mobius). +line/ring/mobius classify correctly and survive 5% noise (3/3 seeds each) on a 2-cycle window. + +KEPT NEGATIVES: + * TORUS needs a window long enough to RESOLVE the two incommensurate tones (Rayleigh, span>=1/df). On a + short 2-cycle window the tones merge into one blurred peak and detection falls back to line rather than + guessing -- a reported limitation, not a silent error (resolved correctly on a long window). + * The MOBIUS (odd-only) basis vs the full ring basis is a TIE on extrapolation under noise (~0.005 each): + MDL on the full-ring basis already prunes the spurious even harmonics, so the odd-only restriction's + value is STRUCTURAL (guaranteed antiperiodicity, half the basis size), not measured accuracy. + +Feeds straight into symbolic_regress (build 2) via a topology-matched dictionary -- no new search machinery. +Tests: +8 in test_holographic_manifold.py (detect line/ring/mobius; noise robustness; accurate period; +matched extrapolates vs flat-line diverges; dictionary shapes incl odd-only; records topology; torus window +requirement; line not forced to ring). 657 -> 665. Additive: a new module; no kernel/compute-path change. + +## B6: Physarum flow-conductance maze solver (Tero et al. 2007) -- shipped + +The elitist-ant slime solver (holographic_slime) is stochastic: random walkers laying pheromone into one +HRR field, needing many rounds + elitist reinforcement on a braided maze to avoid a longer tube. +holographic_flow.py implements the PRINCIPLED dynamics the organism actually uses (Tero, Kobayashi, +Nakagaki 2007): the maze is a tube network, flux from source(start) to sink(goal) is a weighted +graph-Laplacian solve L p = b (Poiseuille Q_ij = D_ij(p_i-p_j), conservation at every node), and tubes +adapt dD/dt = f(|Q|)-D with saturating f(Q)=|Q|^mu/(1+|Q|^mu). Iterate solve->adapt and the network +collapses onto the shortest source-sink path. + +MEASURED vs the elitist ant on braided 16x16 mazes (same maze, same optimum): both find the OPTIMAL path +(seeds 3/7/11/15 -> 84/38/46/42 steps); Tero is DETERMINISTIC (identical reruns) and ~100-340x FASTER +(~90ms vs the ant's 10-32s). The bar -- beat elitist-ant on the braided maze at equal cost -- cleared +decisively. Path extracted by thresholding surviving tubes (BFS), falling back to a widest-path +(Dijkstra on 1/D) route. + +KEPT NEGATIVES / scope: Tero is CENTRALIZED -- each step solves the WHOLE graph's Laplacian (O(N^3) dense), +whereas the ant is decentralized (local diffusion, one holographic field, plus the hierarchical partition +for huge mazes). So this is the principled-physics complement to the holographic ant, operating on the +DECODED adjacency, not itself a holographic method. It needs an explicit source+sink (the ant can diffuse +with no goal). The Baker/Rosetta-seat extension -- fragment assembly as a flow over an energy-conductance +landscape -- is NOT built; this delivers the maze bar, which was the gate. + +Tests: +6 in test_holographic_flow.py (optimal on braided mazes; deterministic; picks short route through a +loop; disconnected/missing-endpoint -> None; wrapper reports optimum + determinism). 665 -> 671. Additive: +a new module; no kernel/compute-path change. + +## Adaptive-rank denoising -- cashing the fixed-rank low-noise negative (shipped) + +B7-original's manifold denoiser (fixed rank-8 projection) had a kept negative: at LOW noise it over-smooths +(projecting onto rank-8 discards real signal detail), measured -0.57 dB harm on real SOL windows. The +teed-up fix (Donoho/Milanfar threshold selection) is now in holographic_denoise.py: + * fit_manifold_full(samples, rank) -> a GENEROUS basis + its singular values. + * estimate_sigma(x) -> Donoho's MAD-of-finest-detail noise estimate (parameter-free). + * adaptive_manifold_denoise(x, basis, mean, sigma=None) -> project, then HARD-THRESHOLD the coefficients + at the universal level kappa*sigma*sqrt(2 ln r) (Donoho-Johnstone shrinkage in the manifold basis). +MEASURED on real SOL windows: at sigma=0.3 fixed rank-8 HARMS (-0.57 dB) while adaptive is neutral (-0.10); +at sigma=0.8 fixed +5.56 dB, adaptive +4.23 dB. The negative is cashed -- adaptive never meaningfully harms +across the noise range. KEPT NEGATIVE: adaptive does NOT match the ORACLE fixed-rank's peak high-noise gain +(when the true rank is known); the value is robustness to UNKNOWN noise, not beating the oracle. A contiguous +top-r* variant was rejected (it truncates real detail at low noise just like fixed rank); individual +coefficient thresholding keeps detail wherever it sits. +5 tests in test_holographic_denoise_adaptive.py. + +## B6 part 2 -- fragment assembly as flow search (the Baker/Rosetta seat) (shipped) + +The maze solver finds a min-cost path on a grid; the Baker/Rosetta seat's fragment assembly -- choose a +fragment per position to minimise an energy, consecutive fragments overlap-agreeing -- is a min-cost path on +a layered (position x fragment) TRELLIS. Same search. holographic_assembly.py builds the trellis (placement +energy encoded as unit hops via relay nodes, so the unit-length Tero solver's shortest path == the min-energy +assembly) and recovers the chosen fragments as a B7 StructureRecipe (each fragment bound to its position). +MEASURED vs exact DP (Viterbi): complete library -> assembles the target EXACTLY (energy 0); with a true +fragment missing -> forced mismatches, and the flow assembly MATCHES the DP optimum (energy 9 == 9), i.e. the +GLOBAL best, not a greedy one. KEPT NEGATIVE: this is the combinatorial CORE (a placement-mismatch energy, a +Rosetta-score stand-in), not a protein force field; the relay encoding bloats the graph by total energy +(fine small; weight edges by length directly for large). +4 tests in test_holographic_assembly.py. + +These two finish the B-list: every breakthrough (B1-B10), the three integration-review items (typed +structure / denoised decode / manifold decompose), B6 (Tero flow) and its fragment-assembly generalisation, +plus the teed-up adaptive-rank denoiser, are shipped and measured with negatives intact. 671 -> 680. + +## Integration plan, Tier 1 -- the DECOMPOSE / DENOISE / FIT faculties wired into UnifiedMind (shipped) + +The integration plan's audit found 14 modules built since the last review with ZERO references in +UnifiedMind: the substrate was shared (every module is bind/bundle/cleanup on the one kernel) but the +orchestration was siloed. UnifiedMind was strong on one half of the loop -- COMPOSE / RECALL / PREDICT / +GENERATE (build structure, act) -- and everything built since is the OTHER half: DECOMPOSE / DENOISE / +SEARCH (take a foreign signal apart, on the right manifold, cleaned). This ships Tier 1: the three +highest-value, mostly-thin-wrapping faculties, each unifying several modules behind one honest entry +point -- the same move B7's typed_structure() made for composition. + + * decompose_signal(x, y) -- one faculty over manifold + symbolic + the multiplicative mode + mobius. + Detect the domain topology (detect_topology), then route the basis: line -> compress_signal(mode= + 'auto') picks an additive OR multiplicative (log-transform) law by the measured conservative rule + (competitive in-sample AND better on a held-out tail); ring/mobius/torus -> decompose_on_manifold's + matched harmonic basis (mobius = ODD harmonics only, the antiperiodic space). Returns a Formula -- + already a savable seed (.generate/.save/.load), the measured-regime twin of a StructureRecipe. + info normalised across both branches: topology, period, mode, n_terms, resid_rms, compression_ratio. + Ergonomic shorthand: decompose_signal(y) fits a lone signal on a unit index grid. + * denoise(x, method='auto', samples=/codebook=) -- one callable over denoise + hopfield. 'adaptive' + (noise-thresholded low-rank projection, the safe default), 'manifold' (fixed-rank), 'codebook' + (modern-Hopfield cleanup), 'nlm' (non-local means on x's own near-duplicates), 'pnp' (Plug-and-Play + /RED restoration with the adaptive map as prior). 'auto' picks codebook if a codebook is given else + adaptive manifold if samples are given. DECISION KEPT HONEST: NLM and PnP stay opt-in -- deciding + self-similar-vs-low-rank automatically is itself a measurement, not faked with an unvalidated + heuristic. And denoise REFUSES a lone vector with no prior (samples/codebook): a denoiser is a map + of a manifold, and there is no free lunch. + * fit_function(X, y) -- the KAN readout as a faculty: a single-layer Kolmogorov-Arnold fit + (HolographicKAN) at this mind's seed, exposing .predict and .feature_function(j, ts). A lone feature + vector is taken as one column. + +KEPT NEGATIVES, surfaced through the faculties rather than buried in the modules: fixed-rank projection +over-smooths at low noise (use 'adaptive', ~neutral there); a manifold projection only helps where real +low-rank structure exists (it destroys structureless signal); NLM only helps where near-duplicates exist; +a single-layer additive KAN cannot represent feature interactions (e.g. x1*x2) -- additive by construction. +And the multiplicative law is auto-selected only on a LINE domain and needs y > 0; a torus needs a window +long enough to resolve both tones or detection falls back to line (the Rayleigh limit). + +THE WIRING IS PROVEN, NOT NOMINAL. The plan's hardest prior lesson (its section 6) was that naive +cross-module chaining once REGRESSED -- a denoiser fed a recall output dropped cosine -- because a shared +KERNEL is not a shared MANIFOLD. So Tier 1 lands with test_integration.py running a cross-faculty pipeline +THROUGH the mind end to end: detect topology -> decompose_signal -> seed.save -> realize (reload + generate) +-> denoise, with each hop's prior matched to its input, asserting the END materially improves on the noisy +input (no silent regression). A faculty that only imports is still a silo; these run. A live confirmation +fell out of writing the test: the pipeline signal sin(x) + 0.4 sin(3x) is purely odd-harmonic, so the +topology detector correctly classified it as MOBIUS (not ring) -- the antiperiodic branch firing on real +input, exactly as the manifold work intended. + +Tests: +7 in test_integration.py (faculties present; periodic-law recovery + bit-exact seed roundtrip + +bounded periodic extrapolation; multiplicative auto-select + single-array shorthand; the end-to-end +pipeline with a no-regression assertion; denoise routing + the honest no-prior refusal + the codebook map; +real-SOL high-noise denoise gain; KAN additive recovery + the interaction-limit boundary). 680 -> 687. +Additive: three new methods on UnifiedMind, each a thin lazy-import wrapper over already-measured modules; +no kernel or compute-path change; fully backward-compatible (new methods only, no signature changes). + +NOT YET WIRED (the rest of the integration plan, sequenced next): Tier 2 -- resolve the factor_composite +duplication by delegating to the B2 SBC resonator (the real de-siloing), decode_structure via peel, and the +opt-in energy cleanup; Tier 3 -- the flow-search and dynamics faculties; Tier 4 -- rd-quant save and the +generative reconcile. This entry covers Tier 1 only. + +## Integration plan, Tier 2 (item 5) -- the factor_composite de-siloing (shipped) + +The plan's "real de-siloing": the audit found TWO factorizers -- the original dense MAP/bipolar +ResonatorNetwork (reached via UnifiedMind.factor_composite) and the newer SBC resonator +(holographic_sbc.decompose_structure / sbc_resonator) with measured higher capacity and a +reconstruction-confidence check -- and called for one. Reading both made the honest shape of the fix +clear, and it is NOT the literal "replace factor_composite's internals" the one-line plan implied: + + * The two factor DIFFERENT objects in DIFFERENT algebras. Dense MAP binding is the elementwise + sign-product (self-inverse); SBC binding is per-block modular addition of one-hots (block-local + circular convolution). The SBC resonator CANNOT factor a dense MAP composite, and you cannot + faithfully transcode an existing dense composite into SBC and recover the same indices. Verified by + reading the modules; the B2 module states it outright ("SBC lives beside the dense kernel, not + inside it"). + * factor_composite's dense contract is PINNED by test_brain_factor_composite (dense codebooks, a + MAP-bound triple, must solve) and by the backward-compatibility rule. So the dense path could not be + deleted -- only delegated-past and deprecated. + +What shipped, the honest version: + * NEW FACULTY UnifiedMind.decompose_structure(composed, codebooks, L) -- the SBC factorizer, which had + NO mind-level entry point before (only a bare module fn and a tour line), is now a first-class faculty + the mind speaks directly. Returns {picks, factors, verified, present}: verified<=>the picks rebuild the + product (it verifies or abstains, never guesses), and present[f] is False when factor f resolved to the + SBC identity (presence detection). + * factor_composite is now ONE entry point: given an `L` it routes to decompose_structure (the preferred, + validated path) and maps the result onto a superset of the old contract (factors/solved + verified/ + present + backend='sbc'); without `L` it runs the legacy dense ResonatorNetwork and emits a + DeprecationWarning steering new code to the SBC path (backend='dense'). The dense return keys are + unchanged, so the pinned test still passes. + +This is genuine de-siloing -- one factorizer for new code, exposed by name -- without faking an algebra +bridge. KEPT NEGATIVE / boundary, on the record: the dense MAP path is RETAINED (deprecated, not removed) +because the SBC resonator is a different algebra that cannot factor dense MAP composites; "one factorizer" +means one PREFERRED, mind-exposed factorizer with the legacy path honestly labelled, not a single +implementation covering both algebras. CI runs plain `pytest -q` (no warning escalation), so the +DeprecationWarning is informative, not breaking; the one legacy test that triggers it now asserts it via +pytest.warns, keeping CI output clean and the intent explicit. + +Tests: +3 in test_integration.py (decompose_structure faculty factors + verifies + presence; factor_composite +routes to SBC and agrees with decompose_structure, presence survives routing; the dense path is +backward-compatible AND deprecated). test_holographic_resonator.py's brain test updated to expect the +deprecation (no count change). 687 -> 690. Additive: one new method + a router on the existing method; no +kernel/compute-path change; the dense path's behaviour and return keys are unchanged. + +NEXT (still queued): Tier 2 remainder -- decode_structure via peel (the B8 denoised per-peel decode), and +opt-in energy cleanup (cleanup(..., energy=True) -> hopfield.dense_cleanup, pinned to argmax at high beta); +then Tier 3 (flow-search + dynamics faculties) and Tier 4 (rd-quant save + generative reconcile). + +## Integration plan, Tier 2 (items 4 & 6) -- decode_structure (peel) + opt-in energy cleanup (shipped) + +The two smaller Tier 2 wirings, finishing the DECODE side. Both modules were already shipped and measured +(B8 peel, B1 dense-Hopfield); the work was exposing them on the mind, with their negatives intact. + + * NEW FACULTIES chain_structure(n) and decode_structure(memory, nodes) -- B7's forward chain object and + its B8 inverse, on the one substrate. chain_structure builds the linked list M = superpose_i + bind(node_i, node_{i+1}) as a StructureRecipe at the mind's dim/seed (realize() gives M); decode_structure + traverses it back by iterated unbinding with PER-PEEL CLEANUP. The crux B8 measured, now visible through + the mind: each recovered pointer is noisy and that noise COMPOUNDS into the next hop, so a raw traversal + (cleanup=None) craters after ~1-2 hops while per-peel cleanup decodes all 15 of a 16-node chain. cleanup + in {None, 'hard', 'soft'}; hard and soft TIE on the discrete pointer (B1's kept negative -- argmax is + Bayes-optimal for identity). Named distinctly from decompose_structure on purpose: decode_structure is the + SEQUENCE inverse (traverse a chain), decompose_structure is the PRODUCT inverse (factor a bound product) -- + different structures, different inverses; the docstrings cross-reference so the pair is not confused. + * OPT-IN ENERGY CLEANUP -- the B1 plan, finally wired exactly where B1 specified: Vocabulary.cleanup gained + an energy=False flag (beta, steps). With energy=True the query is first denoised by the modern-Hopfield + update z <- V^T softmax(beta*V z) against the candidate codebook, THEN the usual nearest-symbol readout + runs. At beta->inf the softmax is one-hot, so the returned identity is BIT-FOR-BIT the plain argmax (the + pinned guarantee). It is a kernel change but purely additive and default-off, so every existing caller is + unaffected (verified: the full core suite passes unchanged). KEPT NEGATIVE, restated: on identity it ties + hard argmax -- the value is cleaning CONTINUOUS vectors (the Tier 1 denoise faculty and peel's soft path), + not changing which discrete symbol wins. + +With these, the mind speaks the whole inverse half of the loop: decompose_signal (a law), decompose_structure +(a product's factors), decode_structure (a chain's sequence), denoise (a manifold), fit_function (a function) -- +each a thin faculty over measured code, each proven through the mind. + +Tests: +2 in test_integration.py (decode_structure round-trips a chain through the mind -- per-peel decodes 15/15, +raw craters <=3, soft ties hard; energy cleanup is opt-in and matches argmax bit-for-bit at high beta). 690 -> 692. +Additive: two new mind faculties + one default-off optional kwarg on Vocabulary.cleanup; no change to any existing +code path (core engine, algebra, peel, typed, recipe, unified suites all pass unchanged). + +TIER 2 IS COMPLETE (items 4, 5, 6). REMAINING: Tier 3 -- one flow-search faculty (solve_maze via flow.solve_maze_flow, +assemble via assembly.assemble) and learn_dynamics (dynamics.Propagator, keeping the market kept-negative); Tier 4 -- +UnifiedMind save requesting quant='rd' on low-rank arrays, and reconciling the generative paths (vector generate -> +hopfield.generate, splat -> scene/archive). Also pending from the modules' own B-list: NEW B7/B8 were the typed +structure and denoised decode (now both wired); no further B-list items remain unshipped per the last close-out. + +## Integration plan, Tier 3 -- the SEARCH & DYNAMICS faculties (shipped) + +Min-cost search (on a graph, on a trellis) and learned linear dynamics, now faculties of the mind. All +three modules were shipped and measured already; the work was exposing them on UnifiedMind with their +negatives intact, and -- where natural -- returning the search result as a B7 typed structure. + + * NEW FACULTY solve_maze(world) -> delegates to flow.solve_maze_flow (the deterministic Tero + flow-conductance model: Physarum tubes thicken with Poiseuille flux until the network collapses onto + the shortest path). Same (path, info) interface as the stochastic slime solver, but DETERMINISTIC and + ~100x faster, and it lands EXACTLY on the optimum on braided mazes (extracted_len == optimal). + * NEW FACULTY assemble(target, library) -> delegates to assembly.assemble, built at the mind's dim/seed. + Rosetta-style fragment assembly (a fragment per position minimising a placement energy, consecutive + fragments overlap-agreeing) cast as the SAME min-cost flow the maze solver runs, on a (position x + fragment) trellis. Returns the assembled string, energy, chosen (pos, fragment) list, and a B7 + StructureRecipe binding each fragment to its position -- the search result AS a typed structure the mind + can realize(). It attains the GLOBAL (Viterbi) optimum, not a greedy one. KEPT NEGATIVE: the energy is a + placement-mismatch / Rosetta-score STAND-IN, the combinatorial core, not a protein force field. Per the + plan, the DP oracle assemble_optimal_energy stays a module reference function, NOT a mind method. + * NEW FACULTY learn_dynamics(states) -> delegates to dynamics.Propagator.learn. Learns a fixed operator U + with state(t+1) ~ bind(U, state(t)) -- in HRR's Fourier domain a per-frequency complex transfer, i.e. the + Koopman/DMD operator in Fourier coordinates. The Propagator exposes .step (one-step prediction = a single + bind), .rollout(state, k), and .recall_at(state, k) (recover the state k steps BEFORE one now). KEPT + NEGATIVE on real SOL returns: prediction only TIES a trivial mean predictor (near-efficient-market returns + have almost no linear structure for a fixed operator to exploit) -- it shines on signals with genuine + linear dynamics (audio, fluids, a bind-shaped control). The CONTENT-ADDRESSABLE round-trip (forward k then + back k -> the start at cosine ~1.0) is the durable win regardless, and is what the integration test pins. + +Tests: +3 in test_integration.py (solve_maze finds the optimal path and is deterministic; assemble is optimal, +matches the exact DP under forced mismatch, and returns a realizable B7 typed structure; learn_dynamics predicts +bind-shaped dynamics far past persistence and round-trips a trajectory at cosine >0.99). 692 -> 695. Additive: +three new mind faculties, each a thin lazy-import delegate over an already-measured module; no kernel or +compute-path change; backward-compatible (new methods only). + +TIER 3 COMPLETE. REMAINING: Tier 4 (lowest urgency) -- (9) UnifiedMind's save path requesting quant='rd' on +low-rank arrays (B5 is already in holographic_core.save; just request it from the mind), and (10) reconciling the +generative paths: point a vector-level generate at hopfield.generate (B10 diffusion) and connect splat (B8) to +the scene/archive representation. + +## Integration plan, Tier 4 -- persistence (rate-distortion) + the generative faculties (shipped) + +The last tier, and the one where the plan's estimate was furthest off: item 9 ("UnifiedMind save uses +quant='rd' -- just request it. Small.") assumed a save path existed. It did NOT -- UnifiedMind had no +to_state/save at all. So this built one, honestly scoped, with a round-trip test (the project's bar for +persistence), which makes it a real feature, not a flag flip. + + * NEW: UnifiedMind.to_state / from_state / save / load, and UnifiedMind registered in + holographic_core._registry() (lazy import, no load-time cycle), so the kernel's versioned save handles + it like every other persistable object and quant='rd'/'auto'/'int8' all apply. save persists the mind's + LEARNED GENERALIZATION: the encoder (perception), the SelfOrganizingMind (the prototype classifier), the + HolographicMind decision brain, and the routing/format bookkeeping classify reads. MEASURED: classify AND + decide are bit-for-bit identical after save->load across quant levels (the encoder, memory, and brain each + already had verified round-trips; this composes them). + DOCUMENTED BOUNDARY / KEPT NEGATIVE: the verbatim recall index of individuals (`_recall`) is NOT persisted + -- its payloads are arbitrary original inputs (raw arrays, dicts, strings) that do not round-trip through a + structured array save -- and the lazy/derived faculties (sequence & plan memory, the text/word generators, + meaning predictors, the scene coder, the FHRR high-capacity memory) are rebuilt on use, not stored. What + round-trips is the trained generalization (classify + decide), proven; recall raises "nothing learned yet" + after a bare load (re-learn for it). Also honest: on a SMALL mind quant='rd' finds no low-rank 2D array to + activate on and falls back to int8 (marginally larger only by per-array qspec overhead) -- rd's ~11x win is + on genuinely low-rank consolidated/bundled state, exactly as the B5 module documents. + * NEW FACULTY generate_vector(codebook) -> delegates to hopfield.generate (B10): generate a hypervector by + denoising FROM PURE NOISE -- anneal beta up and injected noise down, walking onto the codebook manifold. + Generation and denoising are the same operation in different regimes; this is the vector-level twin of the + text generate(). KEPT NEGATIVE: over a bare codebook it converges to a stored atom (degenerate) -- feed a + composed/continuous manifold for novel-but-valid samples. + * NEW FACULTY splat_field(target, k, denoise=False) -> delegates to holographic_splat: represent a 2-D field + as a SUPERPOSITION of K Gaussian primitives by matching pursuit (a splat scene IS a bundle; the RBF + ScalarEncoder is already a Gaussian splat in hypervector space). Reconstructs compactly and, with + denoise=True, denoises (smooth Gaussians have no capacity for noise). KEPT NEGATIVE / SCOPE: isotropic + splats + fixed scales (the honest matching-pursuit baseline); anisotropic covariances, gradient refinement + (full 3DGS), and storing archive images AS splat bundles are documented build targets, not done here. + +Tests: +3 in test_integration.py (the mind save/load round-trips classify AND decide identically with quant='rd', +and the un-persisted recall index raises after a bare load -- the boundary asserted; generate_vector lands on the +manifold and is seed-deterministic; splat_field reconstructs >25 dB and denoises). 695 -> 698. Additive: four new +mind methods (save/load/to_state/from_state) + two generative faculties + one lazy registry key in core; no change +to any existing save/load logic or compute path (the full persistence suite passes unchanged). + +=== THE INTEGRATION PLAN IS COMPLETE (Tiers 1-4, all ten items). === +UnifiedMind now speaks both halves of the loop on one substrate: + FORWARD : perceive / classify / recall / decide / generate (text) / compose (recipe, typed, nested scene) + INVERSE : decompose_signal (a law) / decompose_structure (a product's factors) / decode_structure (a chain) / + denoise (a manifold) / fit_function (a function) + SEARCH : solve_maze (Tero flow) / assemble (fragment assembly, as a typed structure) + DYNAMICS: learn_dynamics (prediction is one bind; content-addressable trajectories) + STORAGE : save / load (rate-distortion), GENERATIVE: generate_vector (B10 diffusion) / splat_field (B8 splats) +The de-siloing is real -- one factorizer for new code (the dense path deprecated, not faked away); the wiring is +proven by test_integration.py running each faculty THROUGH the mind end to end (the §6 lesson: a shared kernel is +not a shared manifold); and every faculty carries its measured negatives. The corollary the plan set out to +enforce holds: there is one MIND the primitives serve, not a drawer of disconnected experiments beside it. + +## Wiring check -- integration plan verified against the live code (clean, with two flagged boundaries) + +Re-ran the plan's own audit (the 14 modules it found with ZERO references in UnifiedMind) plus a +faculty-presence + full-integration-suite pass. Verdict: every plan item is wired and works; two honest +boundaries are flagged below (neither a missed item). + +Module references in UnifiedMind now (was 0/14): + * 12/14 wired DIRECTLY: symbolic, kan, sbc, peel, manifold, flow, assembly, hopfield, splat, dynamics, + denoise (+ the antiperiodic concept via manifold). + * ratedistortion: 0 direct refs but reachable TRANSITIVELY -- UnifiedMind.save -> holographic_core.save + (quant='rd') -> holographic_ratedistortion. Intended (the plan said rd lives in core.save; the mind now + requests it). VERIFIED reachable. + * machine (HoloMachine VM): 0 refs -- INTENTIONALLY standalone per plan §5 ("a VM, adjacent to the mind, + not a faculty"). Correct. + +Faculty presence (live UnifiedMind, all callable): decompose_signal, denoise, fit_function, chain_structure, +decode_structure, decompose_structure, factor_composite (routing+deprecated dense), solve_maze, assemble, +learn_dynamics, save/load/to_state/from_state, generate_vector, splat_field; plus Vocabulary.cleanup(energy=). +UnifiedMind public-method count 86 -> 99. All 18 integration tests pass. + +Duplication table (§4) -- all four rows now closed: + * factor_composite -> SBC resonator: DONE (routes to decompose_structure on L; dense deprecated). + * vector generate -> hopfield.generate (B10): DONE (generate_vector). + * save path -> quant='rd': DONE (UnifiedMind.save requests it via core.save). + * compress_lossless vs symbolic/recipe: the one row left as a doc cross-link -- now CLOSED: compress_lossless's + docstring documents the boundary (lossless entropy coding of discrete TOKENS vs decompose_signal's lossy + generating LAW over a CONTINUOUS signal; both kept, different levels). + +TWO FLAGGED BOUNDARIES (honest, neither is a missed plan item): + 1. holographic_mobius MODULE: the plan's only mobius reference was "(mobius for the antiperiodic basis)" in + decompose_signal -- that role IS wired and tested (manifold.detect_topology classifies 'mobius' and + manifold_dictionary builds the ODD-harmonic basis itself; sin(x)+0.4sin(3x) decodes as mobius). But the + standalone holographic_mobius module -- its AxialEncoder (the double-angle map for AXIAL data: theta == + theta+pi, orientation/director fields) and antiperiodic_split helper -- is NOT on the mind's call path + (referenced only in a docstring, the tour, and its own tests). The AxialEncoder is a distinct ENCODER + capability the plan never listed for wiring; it remains a standalone study, like machine. CANDIDATE for a + future encoder faculty (e.g. perceive(..., axial=True)) if wanted -- not done, not claimed. + 2. splat -> archive: splat_field is wired (a 2-D field as a Gaussian-splat superposition + denoiser). The + DEEPER integration item 10 gestured at -- storing ARCHIVE images AS splat bundles beside the WHT plates -- + is the addendum's documented build target and is NOT done. splat is connected to the mind as a faculty, + not (yet) fused into the archive store. + +No code paths regressed (persistence, brain, organizer, scene, relations, schema, resonator, algebra suites all +green). The only change this check made was the compress_lossless docstring cross-link. + +## Axial perception + the splat-bundle archive (shipped) -- the two wiring-check boundaries closed + +The wiring check flagged two modules whose CAPABILITY was reachable but whose own code was not on the +mind's path. Both are now wired, each through its real published method, each measured. + +AXIAL MODALITY (holographic_mobius.AxialEncoder -> the encoder). An axial value is one where theta and +theta+pi mean the SAME thing -- an unoriented line, a director/nematic field, a crystal axis. On a circle +they sit apart; the fix is the double-angle map theta -> 2*theta onto RP^1 (the Mobius base). UniversalEncoder +now builds an AxialEncoder(dim//2) and exposes modality="axial": it takes the real [Re, Im] embedding of the +phasor, which PRESERVES the FHRR cosine and lands the value in the SAME real space as every other modality, +so the one memory can learn / classify / recall orientations correctly. UnifiedMind gains axial_similarity() +and decode_axial(). MEASURED (dim 512): sim(theta, theta+pi)=+1.00 (same orientation) where the plain number +modality gives +0.76 and cannot see the flip as identity; sim(theta, theta+pi/2)=-0.17 (orthogonal); decode is +mod pi (1.2 and 1.2+pi both read 1.20); and a flipped A-orientation still classifies as A. OPT-IN: a bare float +infers as "number" (infer() cannot tell axial from a plain angle), so axial must be declared. + +SPLAT-BUNDLE ARCHIVE (holographic_splat + new holographic_splat_archive). A 2-D field is a SUPERPOSITION of +Gaussian primitives -- a bundle. SplatArchive stores a gallery as splat codes (cy, cx, amp, sigma) per channel +BESIDE the WHT-plate archive. Because matching pursuit orders splats by decreasing residual energy, the stored +list is already importance-sorted, which buys: PROGRESSIVE REFINEMENT for free (recover(i, k) renders a k-prefix +-- a coarser valid preview; gallery 27.3 dB full vs 19.6 dB at K/4), an EXACT region query (the splats whose +centre lies in a box ARE what is there), and a fixed tunable byte budget. holographic_splat also gains the +addendum's named HRR functions: splat_bundle() encodes a scene as ONE hypervector (quantised per-region peak +occupancy bound to region roles, bundled) and recall_region() reads a region back by unbinding its role and +cleaning up against orthogonal level atoms -- RELIABLE (100% exact-level recall up to 36 regions at dim 4096) +but COARSE (a quantised occupancy, not the splats). UnifiedMind gains splat_archive(). + +KEPT NEGATIVES (measured, not hidden): + * The splat archive is LOSSY and, on the DCT-friendly _gallery, the WHT plates BEAT it on quality at a matched + byte budget (WHT keep=120 reconstructs near-exactly: 163.7 dB at 75 KB vs splat 27.3 dB at 55 KB). The + addendum's "match or beat WHT quality" bar is NOT met for quality on these smooth images -- DCT is ideal for + gradients. The splat archive's real value is the ADDED region-query + progressive-refinement + compact code, + not quality parity; it sits BESIDE the plates, not in place of them. No damage-tolerant joint recovery either + (the plates' strength under erasure). Isotropic splats only (anisotropic covariances / gradient refinement = + full 3DGS, out of scope). + * recall_region is coarse (quantised levels), not a continuous descriptor; the exact per-splat content is + SplatArchive.region. + +LESSON BANKED: this engine's unbind is unbind(composite, key) -- composite FIRST. A reversed call +(unbind(key, composite)) returns ~orthogonal noise and silently destroys recall; it cost a full mis-diagnosis as +a "VSA capacity cliff" before a one-line bind/unbind round-trip check (cosine ~0, not ~1) exposed the real cause. +Always sanity-check the round-trip before blaming capacity. + +Tests: +3 (axial modality theta==theta+pi incl. flip-invariant classify; splat archive recover/refine/region/ +recall; splat_bundle superposition carries region signal). 698 -> 701. + +## Two investigations that did NOT earn a build (kept negatives + the mechanism, so they aren't re-tried blindly) + +Two reframes were proposed -- "bitspace as a loss surface" and "primes as local minima" -- and prototyped +and measured on the real substrate (exp_bitspace.py / exp_primes.py, not shipped). Both are accurate +DESCRIPTIONS of things the engine already does, but neither produced a refinement worth shipping. The +measurements and the reason each failed: + +BITSPACE AS A LOSS SURFACE -- per-component bit allocation vs B5's single global step. B5 +(geometry_preserving_code) quantizes all KLT coefficients with ONE delta found by bisection to hit a target +mean cosine. Tested a per-component allocation that greedily descends a pairwise-cosine-error surface over +bit-allocations, at a MATCHED bit budget, on a controlled low-rank codebook and on real SOL price windows. + * B5 already achieves recall@1 = 0.94-1.00 at every budget tested (down to target_cos 0.97). There is + essentially NO recall headroom to recover. + * On idealized low-rank data the per-component code MATCHES B5's recall and roughly HALVES the pairwise- + geometry error at the same bits (e.g. 0.019 -> 0.009) -- a real but modest win on a metric recall does + not need. + * On REAL SOL windows the per-component greedy is WORSE: it stalls at ~45 bits (flat-ish spectrum, no + dominant low-rank structure to allocate selectively) and gets recall 0.74 vs B5's 0.94-0.99. The single + global step is the right move for near-flat spectra. + WHY: the KLT orders directions by BETWEEN-vector variance, so one global step already crushes mainly the + non-discriminative directions, and a uniform step is the water-filling solution for the retained + components. B5 is already near rate-distortion-optimal for the recall geometry. Not worth the squeeze. + +PRIMES AS LOCAL MINIMA -- log-prime matching pursuit as a factorization/compression algorithm. The idea: +decompose a value's log as a sum over a log-prime basis {log2, log3, log5, ...} by greedy residual descent, +so prime-power values land on exact lattice minima. + * The ALGORITHM FAILS. Coordinate/greedy descent over log-primes recovers ONLY pure powers of the first + basis prime (2^16, 2^10 correct); EVERY multi-prime smooth number is recovered WRONG -- 3^10 -> "2^16", + 360 -> "2^8", 2^6*3^6 -> "2^16". Reason: smooth numbers are DENSE in log space (2^16 ~= 3^10 to 0.10 in + log), so the residual landscape is a thicket of shallow SPURIOUS near-minima, not clean isolated ones; + greedy descent lands on the wrong one. The "local minima" intuition is actively misleading here. + * EXACT factorization (trial division -- not matching pursuit) does work: ~2x on a smooth-integer signal + (901 vs 1680 raw bits). But it is WORSE than raw on functional signals and worse than nothing on random + data, and -- decisively -- the engine's own symbolic compressor nails a functional signal like y=3x^2 + EXACTLY (44 bits, residual 5e-13) where the prime code spends 723. Prime factorization only helps on + smooth-INTEGER signals with no functional form, which the engine does not process (its inputs are float + hypervectors, prices, structured states). + WHY: the reframe restates the already-documented observation ("prime powers compress dramatically; large + primes/random do not") but does not become a buildable capability -- the MP version is mathematically + wrong, and the exact version has no home in the engine's data. Not worth the squeeze. + +THE COMMON THREAD (the part worth keeping): both phrases name the move the engine already makes -- pick the +representation where the hard thing becomes a downhill walk (log turns x into +, KLT decorrelates, the +Hopfield energy turns recall into descent). They are good descriptions of the design, not new leverage over +it. Measured, written down, moved on. + +## Honesty woven into recognition -- calibrated confidence + abstention as CORE (shipped) + +The honesty layer (holographic_honesty: RecallNull / SPRTRecall / bh_fdr) was a standalone MEASUREMENT +harness -- the tour, the tests and holographic_ablate.py called it to VALIDATE the engine, but the mind +itself never used it. It is now part of how the UnifiedMind RECOGNISES, on both readout paths. + +The move it encodes: a raw recall cosine means nothing on its own (radio-SETI and particle physics live by +this) -- you ask how high pure NOISE reaches against THIS codebook before believing a match. RecallNull +draws random unit queries against the mind's own prototypes and records the best cosine each reaches; that +empirical null IS the noise floor, and pvalue(score) = the fraction of noise reaching `score` or higher = +the honest false-alarm probability. Small p: trust the recall. Large p: ABSTAIN. + +Wired (all auto-maintained on the mind's OWN data -- no external calibration set, no new persisted state): + * _recognition_null -- a RecallNull over the class PROTOTYPE codebook (memory.live._stack()), rebuilt + only when the prototype set changes (keyed on the store mutation counter _gen), so steady state is free. + * recognize(x) -- CORE calibrated recognition: (label, similarity, pvalue). + * classify(x, abstain=alpha) -- the label only if p <= alpha, else (None, sim). Default abstain=None + preserves the original always-name-a-nearest-label behaviour EXACTLY (the (label, score) tuple shape is + unchanged), so every existing caller is untouched. + * recall_calibrated / recall(x, abstain) -- the SAME treatment for the INDIVIDUAL store (a _recall_null + over a capped sample of self._recall.vecs), so BOTH memory readouts can say "I have nothing like this". + (Exact-scan winner -- on a large store it can name a truly-nearest item the sublinear forest misses -- + and the capped sample is a documented under-estimate of the true floor.) + * stream_recognize(cues) -- Wald's SPRT over a stream of cues bearing on the same thing; null density = + the mind's noise floor, match density = its own examples' self-similarity (the quantity coherence() + reads). Decides MATCH / REJECT as fast as the evidence allows. + * recognize_batch(queries) -- bh_fdr (Benjamini-Hochberg/Yekutieli) over the per-query p-values, so + scanning many queries cannot manufacture matches by luck (the look-elsewhere discipline). + +MEASURED (dim 512, three single-token text classes): a learned member recognises at p=0.000; gibberish at +p~0.5 so classify(abstain=.05) returns None; the SPRT stream of canine cues decides MATCH in 1 sample +(well-separated densities); the FDR batch keeps the 3 real members and drops the gibberish; recall abstains +on an unseen query. KEPT NEGATIVE / scope: single-token text only matches what was literally learned (no +co-occurrence), so an UNLEARNED synonym like 'terrier' is correctly noise to this mind -- the honesty layer +flags it, which is the point, not a failure to generalise. + +AUDIT (the second half of the task -- is anything else "just a callable method" that should be core?). +Enumerated all 103 public UnifiedMind methods. Finding: honesty was the ONE cross-cutting *property* (vs +*operation*) that belonged in the core, and it is now there, on both readouts. The rest fall into two +groups, both correctly placed: + * the core loop itself (perceive / learn / classify / recall / recognize / decide / reinforce / save), and + * on-demand TRANSFORMATIONS (decompose_signal, denoise, fit_function, decompose_structure, + factor_composite, chain/decode_structure, solve_maze, assemble, learn_dynamics, generate_vector, + splat_field/archive, typed_structure, compose/decompose scene/nested, blend, ...). These are things you + INVOKE, not background properties of recognition; forcing them into the core loop is the "a faculty must + earn its method" anti-pattern the integration plan warns against. +denoise is DELIBERATELY standalone for a MEASURED reason (integration plan section 6): a denoiser fed a +recall output dropped cosine 0.13 -> -0.06 -- a shared kernel is not a shared manifold -- so chaining it +into recall REGRESSES. ONE opportunity is flagged, NOT forced: the calibrated null could replace the +organizer's fixed-floor novelty(0.35) and DRIVE reorganization (reorganize when calibrated-novel inputs +accumulate, not on a fixed schedule). That is a change to AUTONOMOUS behaviour and needs its own measurement +plus a design decision (the learn buffer has a prototype-formation lag), so it is recorded here as a +measured follow-up rather than wired blind. + +Tests: +4 honesty integration tests through the mind (recognize calibrated + classify abstain; SPRT stream +MATCH vs REJECT; FDR-controlled batch; recall abstains on unseen). 701 -> 705. + +## Reorganize when INCOHERENT, not on a clock -- a kept negative (calibrated novelty) and a win (coherence gate) (shipped) + +The audit that wove honesty into recognition flagged ONE opportunity it did not take blindly: the +calibrated noise floor could DRIVE reorganization -- reorganize when calibrated-novel inputs arrive, +not on a fixed schedule. "Build it and let's find out", so this is the measured outcome. + +Setup (exp_calibrated_maintain.py, scratch -- not shipped). A prequential stream where reorganization +genuinely matters: each class is two ANTIPODAL modes on a circle, so the class centroid collapses and the +single (blurry) prototype online `add` keeps is useless -- only a SPLIT (auto_reorganize's job) classifies +it. Two new classes arrive mid-stream, so a trigger's RESPONSIVENESS shows up in post-shift accuracy. The +honest frame: auto_reorganize is SELF-VALIDATING (it holds out recent data, tries k=1..4, adopts the best, +defaults "keep"), so running it never hurts accuracy -- it only costs compute. The question is therefore +the accuracy-vs-COST frontier: hold accuracy with FEWER expensive passes. + +NEGATIVE (the flagged idea). A calibrated-NOVELTY trigger does NOT work. 8 seeds: 75.5+/-9.5% overall, +46.9+/-6.4% on the new classes -- the FLOOR -- at 3.4 passes. It fires rarely and ineffectively because +NOVELTY detects "matches nothing", but online `add` always leaves SOMETHING to match, so the signal stays +low even when the store badly needs reorganizing. And CALIBRATION added nothing over the organizer's fixed +cosine floor (novelty(0.35)): both sat at the ~45% floor. The value of reorganizing here is fixing +INCOHERENCE, which novelty cannot see -- a standing property, not a new-thing-arriving event. + +WIN (what the negative pointed to). COHERENCE -- mean similarity of recent inputs to their own prototype -- +IS the signal. A coherence-gated trigger (reorganize when coherence drops below a floor) gets, 8 seeds, +85.5+/-1.6% overall at 5.8 passes: it BEATS the comparable fixed schedule (k=80: 82.9% at 8.0 passes) on +BOTH accuracy and cost, and matches the best schedule (k=40: 86.8% at 16.0 passes) at about a THIRD of its +passes -- by reorganizing only when the store is actually incoherent and skipping the passes a coherent +store does not need. + +Wired into UnifiedMind as an OPT-IN coherence_floor (default None -> the original fixed schedule, so every +existing test is untouched). One subtlety that mattered: the gate must read a RESPONSIVE coherence window -- +the default window=400 is too smooth to register a mid-stream shift (it left the gate stuck at the 44% +floor), so the gate reads coherence(window=check_every), checked EVERY observation, with a cooldown of +check_every//2. MIND-LEVEL verification (dim 512, check_every=40, 4 seeds) replicates the organizer result: +schedule 86.6% overall / 88.7% new at 16 passes; coherence gate 86.2% / 78.2% at 6.2 passes -- the SAME +overall accuracy at ~1/3 the passes (the gate is slightly less aggressive on the brand-new classes, but +stays well above the floor). The right floor is DATA-DEPENDENT (the coherence scale moves with dimension +and class structure), so it is a parameter, not a constant -- the kept caveat. + +Tests: +2 (coherence gate reorganizes fewer times than the schedule at comparable accuracy and above the +never-reorganize floor; coherence_floor round-trips through save/reload). 705 -> 707. + +## Tier-0 panel fixes: sublinear+calibrated recall, a procedure-matched null, rd-in-auto, calibration coverage (shipped) + +The panel reviewed the live mind and asked first for FIXES to what the honesty/coherence work had just added, +not new features. Four landed. + +1. recall_calibrated was sublinear-DEFEATING (Pharr). It did its own exact O(n) scan for the winner, throwing +away the HoloForest the recall path uses. Now the winner comes through recall() itself -- the sublinear forest +on a big store, the exact scan on a small one -- so honest abstention costs nothing the acceleration structure +did not already cost. Verified: on a 5000-item store the null fit + recall stays ~1s (forest), and +recall_calibrated now returns the SAME winner and score as recall(). + +2. The recall null was ANTI-CONSERVATIVE (Cranmer). It was a RecallNull fit on a capped SAMPLE of the +individuals (max-over-sample < max-over-all), which under-estimates the floor and inflates false matches. +Replaced with a PROCEDURE-MATCHED null: draw random unit queries, run them through the SAME recall path, and +take the score distribution that produces. Calibrated by construction (the null IS what noise scores under the +real procedure) and it inherits the procedure's sublinearity. The earlier "documented under-estimate" is gone, +not just noted. + +3. A calibration COVERAGE diagnostic (Cranmer). calibration_report(n) draws pure-noise vectors and reports the +empirical false-alarm RATE -- the fraction whose p-value falls at or below each alpha -- on both readout paths. +MEASURED: at alpha 0.01/0.05/0.10/0.20 the prototype path fires at 0.008/0.059/0.098/0.200 and the individual +path at 0.008/0.062/0.093/0.200 -- it tracks alpha, so thresholding at alpha holds the false-alarm rate at +alpha. This is the radio-SETI / HEP coverage check, run on the mind's own geometry, and the proof that the +abstention the honesty layer added is trustworthy. + +4. The mind's default save now uses B5 where it helps (Duda). B5's rate-distortion code was in the kernel but +only reachable via quant='rd'; the default 'auto' never asked for it. Now 'auto' itself tries the +rate-distortion code for LARGE low-rank 2D arrays (>= 256 rows), taking it only when it beats int8 and only +because it preserves cosines to 0.9999 (tighter than int8's ~0.998, so it fits auto's decision-safe contract). +Small minds are untouched (rd needs >= 256 rows); a 512x256 rank-8 array drops to ~200 bits/vector vs int8's +2048 (~10x) and round-trips at >= 0.999 row-cosine. The mind's default save now uses the rate-distortion code +automatically wherever the state is genuinely low-rank. + +Tests: +4 (recall_calibrated agrees with recall and can abstain; recognition p-values are calibrated on noise +for both paths; auto picks rd for a large low-rank array and round-trips decision-safe; the mind's default save +round-trips classify-identical). 707 -> 711. + +## Honesty reaches action; the SPRT's real regime; an auto coherence floor (shipped) + +Three pieces: the flagship carries the calibrated-recognition idea from PERCEPTION into the decision brain +(Togelius's seat -- an agent that knows when it is guessing), plus two Tier-0 finishers. + +1. Calibrated decide (Togelius). The creature brain already returns a `support` from value() -- the best cosine +the current state reaches against an action's prototypes -- and used a HAND-SET absolute `blind_floor` on it, +the same uncalibrated-threshold problem the coherence floor had. decide_confidence(state) now turns that raw +support into a false-alarm p-value via a PROCEDURE-MATCHED brain null (_brain_null: run the brain's own value() +on random unit states, take the best-support distribution -- calibrated by construction, value() used as a +black box). It returns (action, pvalue): p small means the brain has genuinely been somewhere like here and the +value estimate can be trusted; p large means it is guessing. decide(..., explore_if_unrecognized=alpha) makes +that actionable -- when p > alpha the value estimate is built on nothing, so take a safe random move among the +allowed actions instead of committing. MEASURED: a familiar state -> the learned-good action at p=0.000; a +never-seen state -> p=0.300; the brain null is calibrated on noise (false-alarm 0.066/0.113 at alpha 0.05/0.10); +explore_if_unrecognized=0.1 spreads a novel state's action ~uniformly (guessing) while a familiar state stays +locked on its trusted action. This is the honesty layer's RecallNull machinery, over the brain's experienced +states instead of perceptual prototypes -- and it replaces the hand-set blind_floor with a calibrated one. + +2. The SPRT's real regime (a Tier-0 demo finisher). Wald's sequential test was decribed as saving ~half a fixed +window's samples, but the tour's distinct learned items are WELL-SEPARATED from noise, so stream_recognize +decides in ~1 sample -- correctly (decide as fast as the evidence allows). The sample-savings appear only when +the match and null densities OVERLAP -- a faint or drifting signal. MEASURED across overlap regimes (matched +error throughout): well-separated -> avg 1.1 samples, fixed-N needs 3 (~64% fewer); overlapping -> 2.2, fixed-N +4 (~46% fewer); heavy overlap -> 4.4, fixed-N 10 (~56% fewer). On REAL noisy cues the count adapts the same way +-- a clear sighting decides in 1, a borderline one spends up to 5. The honest framing: the SPRT is correctly +decisive when the evidence is strong; its efficiency is a property of the OVERLAP regime, not a number to force +on separated densities. + +3. An auto-calibrated coherence floor (a Tier-0 finisher). The opt-in coherence gate fired below a hand-set +absolute level (0.65) that depends on dimension and structure. coherence_floor='auto' removes the absolute +level: track recent coherence and reorganize when it drops below ~90% of its own recent PEAK -- a RELATIVE +retention that transfers across data scales. Honestly, this trades an absolute parameter for a relative one, +not for nothing; but the relative one needs no per-dataset retuning. MEASURED (6 seeds, the antipodal-bimodal +shift stream): fixed schedule 81.1% at 11.0 passes; hand-set 0.65 -> 80.0% at 6.5; AUTO -> 82.1% at 6.7; +never-reorganize 48.5% (the floor). AUTO matches the hand-set floor's accuracy-vs-cost with no absolute +threshold. The 'auto' sentinel round-trips through save/load like a numeric floor; the baseline resets after a +reorganize because the store has changed. + +Tests: +5 (decide_confidence low for familiar / high for novel; the brain recognition null is calibrated on +noise; explore_if_unrecognized guesses randomly on novel states and commits on familiar; the SPRT spends more +samples as densities overlap and beats fixed-N at matched error; the auto coherence floor matches the hand-set +floor without an absolute threshold and round-trips). 711 -> 716. + +## The scan faculty: streaming detection + look-elsewhere control in one pass (shipped) + +A1 from the revised backlog -- the last piece of the honesty arc, and the one Tier-1 item with zero code: +Siemion's seat asked for a single faculty that scans an astronomical channel count the way SETI must -- +decide each channel as fast as its own evidence allows, AND control the trials factor across all of them. +`scan(channels, alpha, beta, fdr)` is pure assembly of parts already shipped: per channel, Wald's SPRT +(B3) decides MATCH/REJECT over that channel's stream of cues; then Benjamini-Hochberg/Yekutieli FDR +(`bh_fdr`) runs across the channels' calibrated p-values. A channel is a CONFIRMED detection only when the +SPRT decided MATCH *and* its p-value survives FDR -- the two disciplines combined. Each channel is a stream +bearing on one hypothesis (a frequency bin over time, a sky position, a recurring pattern). + +The load-bearing detail was a calibration bug caught and fixed before shipping -- the engine's usual lesson. +The channel p-value needs a noise floor for the channel's mean score, and the obvious floor -- the existing +`_recognition_null` -- is WRONG twice over: it scores prototype ROWS (noise mean ~0.086), but recognize() +returns the max LABEL score (sub-prototypes aggregated); and recognize() first runs perceive(), which is NOT +the identity even for a raw vector -- it lifts the vector onto the encoder geometry, raising the max label +score of pure noise to ~0.117. Calibrating to either wrong floor made 69-76 of 80 pure-noise channels look +significant. The fix is a PROCEDURE-MATCHED floor (the recurring principle): run random unit vectors through +recognize() itself -- the exact path a channel cue takes, perceive and routing included -- and resample the +channel-mean null from that, by channel length. With the right floor, pure-noise channel p-values are uniform +again (~8 of 80 below 0.10, as a calibrated detector should be). + +Measured (a weak/drifting target, 256-d, eight clear + four faint signal channels among eighty noise): + +- **Detection.** All eight clear signal channels and all four faint ones detected; zero of eighty noise + channels detected -- false-discovery proportion 0.00 at an FDR target of 0.10. +- **The look-elsewhere value.** Across the eighty pure-noise channels, naive per-channel thresholding at + p<=0.10 flags ~11 false positives (as uniform p-values should); BH-FDR holds the detections to 0. That gap + is exactly the trials factor the FDR controls. +- **The sequential value.** The SPRT spends 1.0 samples on a clear channel but ~1.8 on a faint one -- it + decides as fast as each channel's own evidence allows, the Wald property, now per channel across a scan. +- **Deterministic** run-to-run (the null draws are seeded per channel length -- Macklin's tie-break rule). + +Tests: +1 (scan detects signals, controls the look-elsewhere -- naive false positives cut by FDR -- spends +more SPRT samples on faint channels than clear, and is bit-identical run-to-run). 716 -> 717. + +## Calibrated soft confidence for the resonator: a graded answer on approximate inputs (shipped) + +A2 from the revised backlog -- Olshausen's resonator network with Cranmer's calibrated detector. The SBC +resonator already had a confidence signal: `verified`, True iff the recovered factors rebuild the product +EXACTLY (precision ~1.0). That certificate is perfect on exact products and uselessly brittle on approximate +ones: the moment the input is a noisy bind, exact reconstruction fails and `verified` goes False even when the +resonator found exactly the right factors. `resonator_confidence` (exposed through `decompose_structure(..., +confidence=True)` and `factor_composite(..., confidence=True)`) adds the graded answer -- (picks, verified, +agreement, pvalue), where `agreement` is the fraction of blocks the factors rebuild (the soft version of the +boolean, which is agreement==1.0) and `pvalue` is a calibrated false-alarm probability. + +The calibration is the whole subtlety, and it is the same lesson scan taught. The obvious null -- the agreement +RANDOM PICKS would score -- assumes a factorization matching ~1/L of the blocks by chance (~0.06 here). But the +resonator OPTIMISES reconstruction, so on pure noise it still manufactures ~0.27 agreement, far above that +chance line. Calibrating to the random-picks null therefore rates pure noise as a near-certain detection +(measured p ~ 0.02-0.003). The honest null is PROCEDURE-MATCHED: the agreement the SAME resonator reaches on +STRUCTURELESS input (random SBCs through the real factorizer), which includes its overfitting. That null is a +property of the search configuration -- stable across different random codebooks of the same shape (mean +0.262-0.269 over three) -- so it is cached per codebook set; the first confidence call pays for it, the rest +are free. + +Measured (B=16, L=16, three factors, eight atoms each; true factors (2,5,1)): + +- **The rescue.** Corrupting one to five of the sixteen product blocks, the resonator still recovers the true + factors every time -- but `verified` is True only at zero corruption, False for all the rest. The calibrated + p holds at 0.010 (trust) straight through, exactly where the boolean fails. Agreement falls smoothly 1.00 -> + 0.94 -> 0.88 -> 0.81 -> 0.75 -> 0.69 as the blocks corrupt. +- **Abstention on noise, calibrated.** Over eighty pure-noise products the median p is 0.84 -- it abstains. + Its false-alarm rate is conservative (3 of 80 below p=0.10, against a nominal 8): block agreement is discrete, + so the p-value is stepwise, and conservative is the safe direction for a detector. +- **The kept lesson on one noise product** (agreement 0.250): the random-picks null gives p=0.022 (false + confidence); the procedure-matched null gives p=0.842 (abstains). Same principle that fixed scan's floor. + +Backward-compatible: `confidence` defaults False, so `decompose_structure` and `factor_composite` return exactly +what they did. Tests: +1 (the rescue holds through three corrupted blocks with verified False; noise abstains +with a controlled false-alarm rate). 717 -> 718. + +## Pluggable assembly energy + structure-compare: the Rosetta move and a fold comparator (shipped) + +A3 from the revised backlog -- the Baker seat, and the first Tier-2 item that was genuinely unbuilt. `assemble` +already cast fragment assembly as the same min-cost flow search the maze solver runs, but with one hardcoded +energy: Hamming mismatch, every disagreement costing the same. That is the documented stand-in, not a Rosetta +score. Two additions make it the real thing. + +**Pluggable energy.** `assemble(target, library, ..., energy=callable)` (and `assemble_optimal_energy(..., +energy=)`, and the mind's `assemble(..., energy=)`) lets the caller supply any non-negative placement energy; +it defaults to the Hamming stand-in, so every existing call is unchanged. The point is the Rosetta move -- not +every substitution costs the same. The energy is rounded to integer hops for the relay-encoded trellis (supply +an integer energy for an exact search; the reported energy is the exact unrounded sum), and the flow search +still finds the GLOBAL optimum under whatever energy it is given (it matches the Viterbi DP). Measured: with a +toy substitution matrix where same-group swaps cost 1 and cross-group swaps cost 4, the target "EAAE" assembles +to "BABE" under Hamming (three plain mismatches, cost 3) but to "EEEE" under substitution (cost 4) -- because +"BABE"'s three mismatches are cross-group B-for-vowel swaps that cost 12 under the substitution energy. Each +assembly is the unique global optimum under its OWN energy, both matching the DP. + +**Structure-compare.** `compare_structures(a, b)` superposes two assembled structures and reads their overlap +two ways: `placement_overlap`, the exact overlap coefficient of the (position, fragment) sets (the shared +local motifs of two folds); and `holographic_overlap`, the SAME quantity read from the SUPERPOSITION via +consolidation -- stack both structures' role-bound (pos (x) frag) vectors and take the effective rank (the +consolidation SVD), where a shared placement is the same vector so the combined rank COLLAPSES by the number +shared, giving (rank_A + rank_B - rank_AB)/min as the overlap. On clean structures the two reads agree exactly +(measured 1.00/1.00 identical, 0.33/0.33 sharing one of three, 0.00/0.00 disjoint) -- the holographic read +validated against the exact count, and the form you use when a structure is only available as a hypervector. + +A tie-break caught the test first, which is on-theme for the Macklin determinism work next: the original test +instance had a Hamming TIE (two assemblies both cost 2), and which one the flow search returned was sensitive to +suite ordering -- it passed alone and failed in the full run. The fix was not to special-case the tie but to +pick an instance with UNIQUE optima (enumerated to confirm), so the chosen assembly is deterministic. Atom names +in the comparator are hashed deterministically (Python's str hash is process-randomised) for the same reason. + +Tests: +1 (the substitution energy changes the optimum and each matches the DP; the holographic overlap matches +the exact placement overlap on identical / partial / disjoint structures; deterministic). 718 -> 719. + +## One iterate-a-projection engine + a determinism audit of the calibrated paths (shipped) + +A4 from the revised backlog -- the Macklin seat, two pieces. + +**One engine under three faculties.** Macklin's observation was that the resonator's alternating cleanup, the +PnP/RED denoise loop, and a position-based-dynamics constraint sweep are the SAME object he builds: project onto +each constraint in turn until they jointly hold. `project_onto_constraints(x, projections, iters, tol, omega)` +(holographic_denoise, exposed as a mind faculty) is that engine -- sweep a list of projections, optionally +under-relaxed (omega<1, PBD's stability trick), with early-stop on convergence. The unification is made +load-bearing, not just claimed: `pnp_restore` now LITERALLY calls it (two projections -- a data-fidelity step +then the denoiser), bit-for-bit identical to before (the denoise suite passes unchanged). Demonstrated as three +instances of the one engine: + +- **POCS.** Alternating projection onto two subspaces (sharing a 1-D direction) converges in 29 sweeps to a + point in their intersection, off-axis residual ~5e-13 -- von Neumann's theorem, and it matches the exact + projection onto the intersection. +- **A resonator.** Given factor-cleanup projections (unbind the others, snap to a codebook) the SAME engine + recovers a bound product's factors at reconstruction cosine 1.000 -- WITH restarts. A single restart converges + to a spurious fixed point (recovered the wrong factors): an honest reminder that the real resonator's restarts + are not decoration, they are how alternating projection escapes the non-convexity. +- **PnP.** `pnp_restore` == `project_onto_constraints([data_fidelity, denoiser])`, bit-identical. + +**The determinism audit (the heart of the Macklin ask), expanded to the new paths.** The assemble tie-break two +items ago -- a test that passed alone and failed under suite ordering -- is the class of bug this audit exists to +catch. Every calibrated/null path added this program (recall_calibrated, decide_confidence, the auto coherence +floor, scan, resonator confidence, compare_structures) is now run TWICE on a freshly rebuilt setup -- so its null +is RECOMPUTED, not reused from cache -- with numpy's GLOBAL RNG scrambled in between. All return bit-identical: +the paths draw only from their own seeded `default_rng(self.seed)`, never the global stream (the thing that, had +it leaked in, would have made results depend on whatever ran before). A clean result, but the value is the +guarantee it locks in -- and the cache-clear in the resonator case makes it test the null COMPUTATION, not just +the cache. Determinism here is not luck; it is audited. + +Tests: +2 (the engine as POCS / resonator / PnP, all three matching; and the determinism audit -- six calibrated +paths bit-identical across a fresh rebuild with the global RNG scrambled). 719 -> 721. + +## The inverse problem through the mind: inpaint an erased plate, and validate the noise estimate (shipped) + +A5 from the revised backlog -- the Milanfar/Ozcan seats, and a DEMO not a build (the re-audit found the machinery +already wired). The genuine work was integration: the PnP/RED loop and the noise-adaptive denoiser were callable, +but there was no clean mind entry for "restore a degraded measurement" -- a caller had to hand-build the forward +and adjoint operators every time. `restore(y, mask=..., samples=...)` is that entry: pass a 0/1 mask and the +forward operator and its transpose are filled in (a diagonal mask is its own transpose), the prior is THIS mind's +adaptive manifold denoiser fit from `samples`. It does NOT reimplement anything -- it delegates to +`denoise(method='pnp')`, so the inverse problem is one mind call built on the existing loop, not a silo. + +Measured end to end, through the mind, on an erased archive plate: + +- The mind's OWN splat archive holds a 40-image low-rank gallery (recover round-trips a plate at 29.9 dB). +- One plate has a 5x5 block (25 of 256 px) erased plus light noise -- the degraded measurement. +- A SINGLE adaptive denoise of the masked input reaches 19.3 dB; `restore` (the PnP/RED LOOP) reaches **38.5 dB** + -- the loop beats the one-shot by **19 dB**. The reason is exactly Milanfar's: the one-shot projection is + dragged toward zero by the erased pixels, while the loop holds the observed pixels to the measurement and fills + only the erased ones from the manifold. Reconstruction-under-erasure as a SOLVED inverse problem, in the mind. + +- **Noise-estimate validation, with its kept negative.** Donoho's MAD estimate is accurate at moderate-to-high + noise (true 0.20 -> estimated 0.221; true 0.10 -> 0.126) and the adaptive denoiser's `sigma=None` self-estimate + matches SUPPLYING the true sigma (identical PSNR-to-clean) -- no oracle needed, which is the whole point of the + adaptive path. The kept negative: at LOW noise it OVER-estimates (true 0.02 -> 0.061), because the estimate + assumes the clean signal is smoother than the noise, and a textured low-rank image's own finest detail inflates + the MAD. It is honest where it holds and honest where it does not. + +Tests: +1 (restore inpaints the erased plate through the mind and beats the one-shot by >5 dB, the archive +round-trips the gallery, and the sigma estimate is accurate at the tested level with sigma=None matching the +truth). 721 -> 722. + +## Capacity / SNR vs the cliff, and calibration coverage vs load (shipped) + +A6 from the revised backlog -- the Plate and Cranmer seats, and the ONE genuinely new diagnostic the re-audit +identified (everything else on the list was either built or a demo). It answers two questions about the same +store geometry in one report, `capacity_report`. + +**Where the store sits relative to the noise-wins cliff (Plate, HRR capacity theory).** Random unit vectors in +D dimensions have pairwise cosine ~N(0, 1/D), so a random query's BEST cosine to N stored rows -- the noise +floor -- is the max of N such, ~sqrt(2 ln N / D) by extreme-value theory. A genuine match sits at a much higher +cosine. The report reads off: + +- `dprime` = (match - floor_mean) / floor_std: the SNR, in noise-sigmas, that a real match clears the crosstalk. + Measured: a roomy store (D=512, 8 classes) sits at d'=23; a loaded one (D=64, 20 classes) at d'=6.6 -- the + diagnostic CAPTURES load, the loaded store visibly closer to the cliff. +- the measured floor vs the HRR bound: 0.063 vs 0.090 (roomy), 0.229 vs 0.306 (loaded) -- the same order, the + measured floor sitting a bit BELOW the asymptotic bound at small N (honest: sqrt(2 ln N) overestimates the + expected max for small N, so the store is slightly safer than the bound says). The geometry follows theory. +- `headroom` = n_cliff / N where n_cliff = exp(D match^2 / 2): the roomy store could grow ~10^50x before the + rising floor reaches the match level; the loaded one only ~10^5x. That enormous high-D headroom IS the point + of distributed codes, now a live readout instead of a slogan. + +**Whether calibrated coverage holds as the store GROWS (Cranmer).** Tier 0 validated the false-alarm rate at a +FIXED store; the open question was whether p<=alpha still holds the rate at alpha as N grows and the floor rises. +The report builds random codebooks of increasing size (64 -> 256 -> 1024) in the mind's D, fits the procedure- +matched null on each, and measures the false-alarm rate: it stays ~alpha (0.006 / 0.04 / 0.062 at alpha=0.05) -- +the null re-fits to the rising floor, so the look-elsewhere discipline is load-robust. Materially above alpha at +the largest N would have meant the null was under-sampling the bigger store; it does not. + +The diagnostic is the capacity complement to `calibration_report` (fixed-store coverage) and `resolution_profile`, +and is deterministic (seeded by the mind -- it passed the A4 audit's bit-identical bar by construction). + +Tests: +1 (the operating point ranks roomy above loaded, the measured floor tracks the HRR bound, headroom is +larger for the high-D store, coverage holds <=~alpha at every load, and the report is bit-identical run-to-run). +722 -> 723. + +## A spectral/audio FHRR modality, and dynamics on audio frames -- closing the market-returns loop (shipped) + +A7 from the revised backlog -- Puckette and Stam, and the close of a loop the dynamics work (B4) deliberately +left open. B4's learned propagator (`learn_dynamics`: state(t+1) ~ bind(U, state(t)), a per-frequency transfer +in HRR's Fourier domain) only TIED a trivial mean predictor on real market RETURNS, the correct result kept on +record -- near-efficient-market returns have almost no linear structure for a fixed operator to exploit. The +honest test was always going to be a signal that DOES have linear structure, and audio is the canonical one. + +**The audio modality (Puckette).** `spectral_encode(frame)` is the phase vocoder in the complex domain: a real +frame's DFT splits into a unit-magnitude PHASOR per bin (the phase -- an FHRR vector, every component on the +unit circle, so it binds / bundles / recalls in `high_capacity_memory` like any minted atom) and a MAGNITUDE +per bin (the timbre). Silent bins take phasor 1 by convention so the phasor vector is unit-magnitude EVERYWHERE, +a valid FHRR vector rather than a spectrum with holes. `spectral_decode` re-attaches the magnitudes and inverts +the DFT, exact to 5e-14 (the phasor key plus the magnitude lose nothing). Encoding several BROADBAND sounds +(fundamental plus harmonics plus a little noise) and cramming them into one phasor trace, each recalls by its +key cleanly (3/3, off-diagonal phasor similarity ~0). **Negative on record:** a pure TONE is too sparse for +phase alone to separate -- its silent bins dominate the unit-phasor encoding (three distinct tones sit at +fhrr_sim ~0.99, indistinguishable), so for sparse sounds the MAGNITUDE carries identity, not the phase. The +modality is honest about which half of the (phasor, magnitude) split is discriminative for which kind of sound. + +**Dynamics on audio (Stam, the proving ground).** A sustained multi-sinusoid framed with a hop evolves frame to +frame by exactly the per-bin phase advance the propagator is built to learn (the same advance `spectral_encode`'s +phasors carry -- the operator and the encoding are two faces of one spectral structure). Through `learn_dynamics`, +held-out one-step prediction error is 0.001, against persistence 1.64 (it ignores the advance, so a hop that +moves the phase a half-turn makes the last frame nearly anti-correlated) and mean 1.00 (it averages the +oscillation away). The propagator beats both by three orders of magnitude -- audio HAS the linear structure +market returns lacked, the loop closed with a positive set against the kept negative. On a HARDER case +(non-integer-cycle frequencies plus noise -> spectral leakage and a corrupted transfer) the error rises to 0.169 +-- approximate, not exact, the honest cost of a fixed operator on non-stationary input -- but still beats +persistence (1.59) and mean (1.00) by ~6-10x. And the content-addressable round-trip holds regardless of signal: +forward four frames then back four returns the start at cosine 1.0. + +Tests: +1 (the modality round-trips exactly with unit-magnitude phasors; broadband sounds recall by key from one +shared FHRR trace; dynamics through the mind beats persistence and mean on a sustained tone; the propagator's +predicted next frame, run back through the modality, matches the true next frame's encoding -- the two faculties +wired; and the forward-then-back round-trip returns the start). 723 -> 724. + +## learn_dynamics on a fluid field -- the second positive against the market negative (shipped) + +A8 from the revised backlog -- the Stam seat, and the third validated regime for the B4 dynamics operator after +audio (A7) and the kept market negative. Stam's "Stable Fluids" and his FFT fluid solver work on a periodic +(toroidal) domain, doing the hard step in Fourier space -- the same FFT-on-a-torus the engine's bind already +is. A passive scalar's LINEAR advection-diffusion step is exact there: in Fourier each mode k just rotates +(advection: phase -2*pi*k*shift/N) and decays (diffusion: e^{-nu*k^2}), i.e. a per-bin complex transfer -- +precisely the operator `learn_dynamics` fits. + +**The clean Stam case.** A bump plus two low modes, advected on a 256-point torus, framed into a sequence of +fields. Through `m.learn_dynamics`, held-out one-step prediction error is 0.011, against persistence 0.34 (the +field has moved, so the old field is stale) and mean 1.15 (averaging the moving structure away). The propagator +recovers the advection-diffusion operator almost exactly -- a fluid field HAS the linear structure a fixed bind +operator exploits, where near-efficient-market returns did not. Two further properties, both measured: + +- **Surrogate solver.** The learned operator rolls out 8 steps from a single field and tracks the true + simulation to ~3.5% relative error -- learn the fluid operator from a handful of frames, then simulate + forward with one bind per step. +- **Content-addressable trajectory.** The operator's own forward-k-then-back-k returns the start at cosine 1.0 + (even with diffusion: the Wiener-regularised inverse exactly undoes the operator's own forward map). The + earlier confusion -- recalling the TRUE future field gave cosine ~0 -- was the honest distinction that an + imperfect learned operator inverts its OWN trajectory, not the ground truth it only approximates. + +**The honest limit, kept on record.** A NONLINEAR Burgers field (u_t + u u_x = nu u_xx) forms shocks -- the wave +steepens, energy cascades to high modes -- and no single fixed LINEAR operator captures that. Measured: the +propagator does WORSE than persistence on a shock-forming Burgers field (error 0.054 vs 0.006; worse still, +0.125 vs 0.015, for a stronger shock). The propagator is for linear or linearizable dynamics; nonlinear flow +with shock formation is exactly where it fails, and that negative sits beside the audio and linear-fluid wins. + +The faculty's own docstring now records all three regimes (audio, linear fluid, the Burgers limit) so the +measured boundary travels with the code. This is a validation of existing machinery through the mind, not a new +build -- the same shape as the PnP restoration demo. + +Tests: +1 (linear advection-diffusion beats persistence and mean through the mind; the operator rolls out 8 +steps as a surrogate within 10%; the forward-then-back round-trip returns the start; and a shock-forming Burgers +field is the honest case where the propagator loses to persistence). 724 -> 725. + +## Multi-terminal network design -- the Tokyo-rail Physarum, as a typed graph-memory (shipped) + +A9 from the revised backlog -- the Adamatzky seat, and a genuine BUILD (not a validation): the multi-terminal +generalisation of the single-source `solve_maze` flow solver. Tero et al. (2010, *Rules for Biologically +Inspired Adaptive Network Design*) showed Physarum grows a network connecting many food sources that rivals the +Tokyo rail network on cost, efficiency, and fault tolerance. `tero_network` reproduces that on a graph: drive +flow between ALL pairs of terminals, and the tubes that survive form the connecting network. + +**The mechanism, with two real improvements over a naive port.** (1) The Laplacian depends only on the +conductivities, so every terminal pair is solved in ONE multi-right-hand-side factorisation per step (A P = B +with a column per pair), not one solve per pair. (2) Summing raw flux over pairs pinned every tube open (the +saturating response f = q^mu/(1+q^mu) hits 1 when flux is large, and most edges carry large summed flux). The +fix is to adapt each tube toward the MEAN saturated response OVER pairs -- i.e. toward how many terminal pairs +route through it -- so trunk tubes thicken and unused tubes die. Without it the network is the whole grid. + +**The cost / fault-tolerance trade-off, measured (7x7 grid, 5 terminals, MST baseline 24 hops).** The `mu` +feedback tunes exactly what Tero describes: +- mu=4: a near-minimal Steiner TREE -- 21 edges, 0 cycles, and SHORTER than the 24-hop terminal-MST (the flow + shares trunk segments through Steiner cells the pairwise-MST cannot), a genuine Steiner approximation. +- mu=2: a fault-tolerant network -- 36 edges with 4 redundant loops (alternate routes that survive an edge cut) + at modest extra cost. +- mu<=1: the full redundant mesh. + +**Wired in as a B7 typed structure.** `design_network` returns the network BOTH as raw edges and as a typed +graph-memory: a StructureRecipe building M = superpose over edges of bind(node_u, node_v) -- the same +construction `chain_structure` uses for a linked list. That realise()s to one hypervector, and the engine's own +unbind + cleanup recalls a node's neighbours: unbind M by a node atom, snap the result to the node codebook, +and the true network-neighbours come back above every non-neighbour (node (0,0) -> {(0,1),(1,0)} at similarity +0.15 vs 0.06 for non-neighbours). The network is not a side artefact; it is an object the mind can store, query, +and decode like any other structure. + +A note kept honest: the dense Laplacian solve is O(n^3) per step, so this is for modest graphs (tens of nodes), +the regime where the flow model's interpretability is the point; a sparse/conjugate-gradient solve would be the +scaling path if needed. + +Tests: +1 (high mu gives a tree no longer than the MST with zero cycles; low mu gives a strictly larger mesh +with cycles; the default network connects every terminal and its typed graph-memory recalls a terminal's actual +neighbours above all non-neighbours). 725 -> 726. + +## Cross-modal recall: the exact image archive, reachable from the mind, queried by description (shipped) + +A10 from the revised backlog -- the Ozcan seat. The re-audit found the cross-modal machinery was ALREADY built +in `HolographicArchive` (the DCT/Walsh-Hadamard plate store): `add(image, tags=[...], nums={...})` attaches a +hypervector address (a bundle of tag atoms, plus bind(attr, scalar.encode(v)) for numeric attributes), and +`recall_by_tags(words=[...])` returns the best-matching image by cosine of the query address to the stored ones +-- Ozcan's describe-then-retrieve. The gap was pure integration: only the LOSSY `splat_archive` was wired into +the mind; the EXACT, tag-addressable archive was unreachable. + +**Wired in** as `image_archive`. The mind now has both archives and the right one for the job: splats for a +compact resolution-independent bundle, plates for bit-exact recall AND cross-modal addressing. Measured through +the mind on a 4-image gallery: + +- **Exact recovery** at full keep (all DCT coefficients): max pixel error 6e-15 -- a single adjoint per channel + inverts the superposition exactly. (Fewer coefficients trade exactness for compression, the archive's other + mode.) +- **Tag -> image**, soft-AND over the query: `['round','large']` returns the ring, `['round','small']` the + circle -- the image matching the MOST query tags wins, because each shared tag adds its atom's correlation to + the address match. +- **Robust under damage**: describe-then-retrieve still reconstructs the gradient from `['smooth']` at 0.002 + error with 40% of the plate ERASED -- the joint masked recovery the archive is built for, now driven by a + text query instead of a degraded picture. + +**The improvement: the reverse direction.** The archive could go tag -> image but not image -> tags. Added +`tags_of(i, candidates)`: rank a candidate vocabulary by each word's correlation to stored image i's address -- +the description the archive would give the image. 'ring' comes back as round + large (0.72 each) over smooth +(0.01). Cross-modal recall is now bidirectional: describe to retrieve, or retrieve to describe. + +Tests: +1 (the mind's image_archive recovers every image exactly; tag queries return the right image including +soft-AND; the reverse ranks an image's own tags on top; and describe-then-retrieve survives 40% plate erasure). +726 -> 727. + +## Generation over a composed subspace -- the B10 sampler's interesting regime (shipped) + +A11 from the revised backlog -- the Eno seat, and the regime B10 (generative denoising) explicitly flagged as +the one worth reaching. B10's sampler runs the denoiser BACKWARDS from pure noise -- anneal beta up, injected +noise down, iterate the cleanup -- and walks a random vector onto the signal manifold, a sample generated by +denoising. Its KEPT NEGATIVE: over a BARE codebook it converges to a stored atom (a degenerate sampler that can +only return what is already in the box). The interesting regime, per the docstring, is a COMPOSED manifold. + +**The composed-manifold denoiser.** A valid composed structure is a bundle over slots of bind(role, filler) for +fillers drawn from a vocabulary -- one of V^S structures, far too many to enumerate as a codebook. So the +denoiser is slot-wise: for each role, unbind the slot, dense_cleanup its filler toward the vocabulary, rebind, +then bundle and renormalise. `generate_structure` drops that projection into B10's annealed diffusion in place +of the bare-codebook cleanup, so the random start walks onto the manifold of role-filler STRUCTURES instead of +collapsing to an atom. (The slot-wise projection is itself an instance of 'iterate a projection' -- the same +shape as the resonator and the PnP loop.) + +**Measured through the mind** (3 slots, 6 fillers, V^S = 216 possible structures, 1024-d): +- **Diversity:** 10 distinct valid structures from 10 seeds -- the sampler explores the composition space, it + does not fall into one attractor. +- **Validity by construction:** re-encoding each generated vector's decoded fillers reproduces it at cosine + 1.0 -- the output genuinely IS a composition (bundle of role-bound fillers), and every slot unbinds to a + vocabulary atom. +- **Not a stored atom:** the generated structure is nearly orthogonal to every single filler (max |cos| < 0.4) + -- a composition, not a verbatim atom. +- **The kept negative confirmed for contrast:** `generate_vector` over the bare filler codebook returns a + single stored atom at cosine ~1.0 (degenerate) -- exactly what generating over the composed manifold avoids. + +So generation and denoising remain the same operation (Eno's process-not-object framing), and pointing that +operation at the composition manifold turns a degenerate atom-recaller into a generator of novel-but-valid +structure -- the bar B10 set, now cleared. + +Tests: +1 (10 seeds each produce a valid composition that re-encodes to itself and is orthogonal to every bare +filler; at least four are distinct; and bare-codebook generation collapses to a stored atom). 727 -> 728. + +## A fractal scene from a single seed vector -- one kernel, repeated to depth (shipped) + +A12 from the revised backlog -- the Quilez seat, the demoscene aesthetic stated in the engine's terms: maximal +richness from a tiny deterministic kernel, infinite detail by recursion. The existing `nested_scene_structure` +does scenes-of-scenes for one level; the ask was a single seed vector driving one kernel repeated to ARBITRARY +depth, with `fractal_dimension` reported. + +**The kernel lives in one vector.** `fractal_seed(offsets, scale)` encodes a fractal kernel -- N copies of the +plane, each contracted by `scale` and translated to an offset -- as a holographic bundle: sum over copies of +bind(pos_role, grid_atom[offset]) plus bind(scale_role, scale_atom). The whole generator is carried in the +geometry of one hypervector. `fractal_scene(seed, depth)` decodes it with pure VSA -- unbind the position role +and threshold the grid atoms to recover WHICH cells are offsets, unbind the scale role and clean up to recover +the scale -- then expands that one kernel to `depth` (each level places a contracted copy of the whole scene, +N^depth points), and reports the box-counting dimension. This is an IFS whose maps are read out of a vector. + +**Measured through the mind:** +- Seed A (a Sierpinski kernel: 3 copies at scale 1/2) decodes to exactly 3 offsets and scale 0.5, and its + expanded scene has box-dimension 1.57 against the self-similar log3/log2 = 1.585. +- Seed B (5 copies at scale 1/3) decodes to 5 offsets and scale 1/3, box-dimension 1.51 against log5/log3 = + 1.465. +- The two seeds give DISTINCT measured dimensions -- the seed genuinely drives the scene -- and expansion is + deterministic (same seed, identical points). The small box-counting gaps (0.01, 0.04) are the expected + finite-sample / finite-range bias of box counting, not error in the construction. + +So a single vector encodes a generator, the engine decodes it, and one kernel repeated to depth yields a +self-similar scene of a predictable, measured fractal dimension -- the SDF/demoscene move (one kernel, domain +repetition, infinite detail, deterministic from a seed) on the VSA substrate. An honest scope note: the offsets +are snapped to a grid codebook so they decode by exact cleanup, and the dimension is set by N and scale (both +recovered exactly), not by sub-grid offset precision. + +Tests: +1 (a Sierpinski seed decodes to 3 copies at scale 1/2 with box-dimension near log3/log2; a 5-copy +scale-1/3 seed lands near log5/log3; the two dimensions are distinct; and expansion is deterministic). 728 -> 729. + +## Anisotropic splats and a 3-D extension -- the real 3DGS primitive, fit from scratch (shipped) + +A13 from the revised backlog -- the Drettakis seat, and a deliberately-deferred Tier-4 scope: the splat work +(B8) used ISOTROPIC (circular) Gaussians by explicit choice; 3D Gaussian Splatting's actual primitive is an +ANISOTROPIC, oriented Gaussian with a full covariance, fit by differentiable optimisation. A13 builds that core +in NumPy, true to the project's minimal-framework rule (analytical gradients + a tiny hand-written Adam, no +autodiff library). + +**The primitive and the fit.** Each splat is (center, amplitude, L), L the lower-triangular Cholesky factor of +the INVERSE covariance, so the Gaussian is amp * exp(-0.5 ||L^T (x - center)||^2) and L lower-triangular keeps +the precision positive-definite for free. `aniso_fit` warm-starts from the isotropic matching pursuit (so the +covariances only have to specialise), then descends the reconstruction MSE with analytical gradients for the +amplitude, center, and L. The whole thing is dimension-general: a 2-D image and a 3-D volume share one fit. +Wired in as `splat_aniso` (the anisotropic, n-D twin of `splat_field`). + +**Measured through the mind** -- anisotropy is decisive exactly where structure is oriented: +- 2-D, two elongated oriented ridges, K=4: isotropic ~18 dB -> anisotropic ~64 dB. A circular Gaussian cannot + match an elongated ridge; one aligned anisotropic splat does, and four nearly reconstruct two ridges. +- 3-D, an elongated ellipsoid, K=3: isotropic ~24 dB -> anisotropic ~61 dB. An ellipsoid IS one anisotropic + Gaussian. +- The learned splats are genuinely anisotropic (inverse-covariance eigenvalue ratio > 3 on the ridges), and + re-rendering the returned (center, amp, L) code reproduces the fit. + +**KEPT NEGATIVE / honest scope.** The loss is non-convex, so the fit finds a LOCAL optimum: more splats do NOT +help monotonically -- a clean K=4 fit (66 dB) beat a messier K=8 one (52 dB) in testing -- and the result +depends on the isotropic warm start. And this is the from-scratch CORE of 3DGS only: no tile rasteriser, no +spherical-harmonic view-dependent colour, no GPU speed; it runs on small fields where the optimisation is the +point, not a real-time renderer. That boundary is the honest Tier-4 label -- the primitive and its +differentiable fit, not the production system. + +Tests: +1 (anisotropic beats the isotropic warm start by >15 dB in both 2-D and 3-D on oriented structure, the +splats are measurably anisotropic, and the splat code re-renders to the fit). 729 -> 730. + +## Tensor-product / tensor-train (MPS) bind vs HRR -- the capacity comparison (shipped) + +A14 from the revised backlog -- the Stoudenmire seat, the heaviest and most speculative item, and the LAST of +the program (A1-A14 all delivered). HRR's bind(a,b) = circular convolution is a compressed projection of +Smolensky's tensor-product binding a (X) b; tensor networks (MPS / matrix-product states) interpolate by +truncating the tensor product to a low bond rank. `tensor_bind` builds the uncompressed outer-product bind and +its rank-r 2-site MPS truncation so all three points on the rank spectrum can be measured against the engine's +circular convolution. The result is bucketed honestly -- a real but storage-bought capability, not a free win. + +**The mechanism.** The outer-product bundle M = sum_i outer(k_i, v_i) recalls value_i as M^T k_i = +sum_j (k_j . k_i) v_j. The crosstalk coefficient is the key inner product (~1/sqrt(D) for random keys), so the +crosstalk is suppressed by 1/sqrt(D) -- whereas HRR's unbind produces full-magnitude pseudo-random crosstalk +vectors. That single difference is why the tensor product recalls so much better at a fixed load. + +**Measured (D=128) against HRR:** +- At a fixed LOAD M=16, recall cosine is 0.25 (HRR) vs 0.95 (tensor product) -- the tensor product is far more + faithful, because it spends D^2 = 16384 numbers against HRR's D = 128. +- With ORTHOGONAL keys at M = D, the tensor product is EXACT (recall 1.000) -- the key inner products are + Kronecker deltas, zero crosstalk -- where circular convolution cannot be (0.10). A genuine qualitative + advantage for structured keys. +- A rank-8 binding matrix (values in a rank-8 subspace) MPS-truncates LOSSLESSLY: recall 0.862 preserved while + storage drops from 16384 to 2048 numbers (8x). Truncating below the true rank (rank 4) is lossy (0.66). This + is the tensor-network's real capability -- exploit low rank / low entanglement. + +**KEPT NEGATIVE (the honest bucket).** At a fixed RECALL THRESHOLD the capacity-per-stored-number of HRR and +the tensor product is the SAME (both ~ (1-t^2)/(t^2 D) per number) -- HRR's compression gives up nothing on +that frontier; it simply chooses the compact, low-absolute-capacity end while the tensor product chooses the +high-fidelity, high-storage end. And a generic (full-rank) binding cannot be MPS-compressed without losing +recall, and even the lossless low-rank form (>= 2D numbers) still costs more than HRR's D. So the tensor / +tensor-train bind is a DIFFERENT point on the storage-vs-fidelity tradeoff -- worth it when you need exact +high-capacity structured recall and can afford the storage, or when an existing bound tensor is genuinely +low-rank -- not a way to beat HRR's efficiency on generic bindings. That is the correct, measured place for it. + +Tests: +1 (the tensor product beats HRR at fixed load and is exact for orthogonal keys; the MPS truncation is +lossless at the true rank and lossy below it, at a fraction of the full storage but still above HRR's). 730 -> 731. + +## Integrating Path D -- federation and width, the "as above, so below" arc (shipped) + +A parallel investigation ("Path D": computing and storing INSIDE the holographic space) was merged in from a +separate session. It arrived as a self-contained bundle -- two new modules, plus experiments, figures, and the +frontier-program / dataset-benchmark / distribution-candidate docs -- and it touched none of the existing +engine code, so the integration was additive: the bundle lives under `path_d/`, the two reusable modules were +hoisted to the top level and (the real work) WIRED INTO `UnifiedMind` as faculties, the same discipline every +other module shipped under. They imported cleanly against the frozen kernel with zero API drift, and both their +selftests and the headline experiments reproduced on this tree before anything was written down. + +**The through-line Path D found.** One D-dimensional vector holds only ~0.1 x D items faithfully (~0.02 x D for +*continuous* compute with no cleanup to absorb crosstalk), and that budget is CONSERVED -- you do not beat it by +encoding harder, you FEDERATE: more vectors = more total dimensions = more capacity, coordinated by a thin layer. +The same move recurs at every scale (storage, lookup, resilience, the neural-network forward pass), which is the +engine's recurring lesson seen one rung up: the within-vector property becomes the across-shard property by the +same linearity. + +**Two faculties wired and measured through the mind:** +- `storage_array` (holographic_array.HoloArray) -- a federated, RAID-style symbol store. A shard is a running + sum, so a PARITY shard is the real-valued sibling of a fountain XOR droplet and reconstructs a lost shard + EXACTLY by subtraction. Measured: 150 symbols at D=1024 auto-grow to 3 shards at ~0.89 recall (one shard would + have cliffed); lose a shard and parity restores recall exactly (0.89), where a zeroed shard drops it to ~0.55. + KEPT NEGATIVE / information floor: `n_parity` parity survives at most `n_parity` losses -- it cannot recover + more than it has parity, mirroring the fountain's "too few droplets -> nothing." +- `superpose_compute` (holographic_superposed) -- the WIDTH faculty: evaluate K computations at once inside one + vector (Kanerva/Kleyko computing-in-superposition), the parallel-readout complement to the mind's DEPTH side + (recursion, `peel` traversal, the inception depth law). Measured: a single keyed item recovers EXACTLY with a + unitary key (cosine 1.0); six candidates packed into one vector are scored in parallel and the winner is + resolved cleanup-gated against a codebook. KEPT NEGATIVE / the conservation law: recovery fidelity decays with + width (mean cosine ~0.50 at K=4 -> ~0.12 at K=64) -- width is bounded, and you buy more by spending DEPTH, not + by widening one flat bundle. + +The bundle's own headline (reproduced here) is the distributed forward pass: a single weight-vector is faithful +to 16 classes (~0.02 x D), and federating to 8 shards holds 96 (~6x) -- the same federation move that fixes +storage, applied to the matmul. The bundle also carries the frontier-program and dataset-benchmark docs (now +under `path_d/docs/`) and a second lever, RNS-phasor arithmetic, that lives in the Path D experiments but is not +yet an engine module -- a clear, honestly-labelled next step rather than a claim. + +Tests: +3 (one mind-level integration test wiring both faculties -- federation grows shards and parity restores +a lost one exactly; superpose is exact at K=1, resolves the right winner, and decays with width -- plus a CI +selftest wrapper for each new module). 731 -> 734. + +## Exact RNS-phasor arithmetic -- the matmul wall was the encoding, not the substrate (shipped) + +P1 of the Path D integration -- the second lever, and the one that had no engine module (it lived only in the +Path D experiments). General matmul read out of a lossy SUPERPOSITION (bundle the matrix rows, unbind, dot the +input, no cleanup) is capped by crosstalk: the bundled rows interfere on readout, so fidelity collapses as the +matrix grows. But matmul is multiply-accumulate of NUMBERS, and the FHRR side of the engine already carries a +number exactly as a phase -- a unit phasor exp(2*pi*i*r/m) IS the residue r mod m, and binding phasors adds +their phases. So a product of phasors is exp(2*pi*i*(sum r)/m): the exact sum of residues mod m, for ANY number +of terms, with no crosstalk. That is the single thing the bundle got wrong. + +`holographic_rns.py` carries each number as residues over coprime moduli (a Residue Number System), does every +multiply-accumulate as that exact phasor-binding modular arithmetic (one channel per modulus), and recomposes +the integer with the Chinese Remainder Theorem. Wired into the mind as `exact_matmul`. + +**Measured through the mind:** +- Modular accumulation via phasor binding is EXACT for thousands of terms (0 errors at N=5000) -- the + crosstalk-free MAC the bundle could not do. +- Integer matmul at M=256, N=64 is EXACT (max|error| = 0), exactly where the lossy superposed readout of the + same matmul manages only ~0.11 fidelity. +- A float matmul (fixed-point, scale given) is exact for the QUANTIZED operands. +- The exact dynamic range FEDERATES over moduli channels (~1e8 with a few -> ~1e62 with more) -- the arithmetic + sibling of the storage array's federation: more channels = more range, coordinated by the thin CRT recompose. + +**KEPT NEGATIVE / scope:** exact for INTEGER / fixed-point operands within range. A float is quantized first and +the only error is that fixed-point rounding (set by `scale`) -- a bit-depth question, separable from and unlike +the crosstalk wall (it does not grow with matrix size). And the FLOPs are real: the parallelism is per-modulus / +per-output, native on phasor or RNS hardware, not free on a CPU. The faculty delegates the phase composition to +the same primitive holographic_fhrr binding uses, one phase channel at a time. + +Tests: +2 (a mind-level test -- exact integer matmul where the lossy bundle gets ~0.11, the FHRR-binding +accumulation identity, fixed-point exactness, and range federation -- plus a CI selftest wrapper for the module). +734 -> 736. + +## Recursive pivot-tree index -- sublinear recall as cleanup applied recursively (shipped) + +P2 of the Path D integration -- the forest / data-structure seat (Pharr), and the cleanest realisation yet of +the engine's long-standing sublinear-retrieval wish. The crash that preceded it is the kept lesson: a content +index that summarizes items UPWARD into a bundle hits the capacity wall, and recall collapses to ~0.23 -- the +bundle blurs as it grows. A B-tree never does that. Its internal nodes hold PIVOTS (separators), stored +explicitly, so the wall never bites; in VSA a node is then a small cleanup memory of (pivot -> child), and +routing a query is a nearest-pivot decision applied RECURSIVELY -- the same `cleanup` primitive the mind already +uses, one level per hop, inception as the addressing fabric. `holographic_pivot.py` builds the tree by recursive +k-means (NumPy only, no sklearn -- the minimal-frameworks rule) and routes with a beam; wired in as `pivot_index`. + +**Measured through the mind** (216 well-separated leaves, so the exhaustive ceiling is high and any drop is the +routing, not the data): +- Greedy top-1 routing matches the exhaustive scan -- ~0.88 vs ~0.90 -- while touching only ~18 pivots instead + of all 216, an order of magnitude fewer comparisons (~O(log N), the tree's whole point). +- A beam-5 search lands the true leaf in the candidate set 100% of the time, after which an exact key-unbind (or + a final scan of the few candidates) finishes -- against the naive summary index's 0.23. + +**KEPT NEGATIVE:** each hop is an approximate nearest-pivot decision, so a wrong turn at beam=1 can lose a query +on overlapping (not well-separated) data -- the beam is the honest knob that buys recall back, trading a few +more comparisons for it. The build cost is the recursive k-means. The routing delegates to the same nearest- +codebook cleanup the mind's recall uses; the tree is just that cleanup stacked, depth-many. + +Tests: +2 (a mind-level test -- greedy top-1 matches exhaustive at a fraction of the comparisons, with beam +recall of the true leaf -- plus a CI selftest wrapper for the module). 736 -> 738. + +## Sketch-routed array recall -- breaking the broadcast wall, content-addressably (shipped) + +P3 of the Path D integration. The storage array already recalls in O(1) when you have the directory (item -> +shard), and routerlessly by BROADCAST when you don't -- but broadcast asks every shard, so it costs O(shards) +and soft-erodes as the array grows (the false-alarm tax of many value-cleanup votes). The fix is the same +content-addressable trick one rung up: summarize each shard by a SKETCH = bundle of its keys (the holographic +'and' of what it holds, one extra vector per shard), match a query's key against the sketches in one matmul to +pick the top-c candidate shards, and unbind+cleanup ONLY those. Routing by key-sketch is a CLEAN decision -- a +key sits ~1/sqrt(load) inside its own shard's sketch, far above the 1/sqrt(D) noise from the others -- so it +stays accurate exactly where the broadcast value-vote drowns. Added to HoloArray as `routed_recall(g, c)` and +advertised on the `storage_array` faculty (which now exposes directory / broadcast / sketch-routed recall). + +**Measured through the mind (64 shards, ~1920 items):** +- directory recall 1.00, sketch-routed(c=8) 0.99, broadcast 0.95 -- routed tracks the directory while broadcast + erodes with shard count (0.97 at 32 shards -> 0.95 at 64), and the gap widens as the array grows. +- routed touches only c=8 of the shards, not all 64 -- O(c) unbinds instead of O(shards), the sublinear win, + while matching the exhaustive directory. + +The sketch is built lazily and rebuilt whenever the shard count changes; it reuses the engine's own +bundle/unbind/derived_atom, adding an index, not a new algebra. (Path D also asked whether a 2-level +sketch-of-sketches buys fully sublinear ROUTING; that runs into the per-vector capacity wall on the upper +sketches and is kept in the experiments as a measured open question, not wired as a claim.) + +Tests: +1 (a mind-level test: sketch routing stays above 0.95 and tracks the directory at 64 shards while +unbinding only c=8 of them, at least as accurate as full broadcast; the module's own selftest gains a 48-shard +routed-recall check). 738 -> 739. + +## Distributed forward pass -- federation applied to compute, with depth cured two ways (shipped) + +P4 of the Path D integration, and the headline the whole "as above, so below" arc was driving at: federation, +which fixed storage, fixes COMPUTE too. A linear layer's weight rows stored in ONE bundled vector cap out at +C ~ 0.02 x D classes -- recovering a row carries crosstalk from the other C-1 rows, and the continuous logit + has no cleanup to absorb it, so fidelity dies as the matrix grows. FEDERATE the rows across K +weight-memory shards (row c in shard c mod K) and recovering a row only carries crosstalk from its ~C/K +shard-mates, so the wall moves to C ~ K x 0.02 x D. `holographic_compute.py` implements the federated readout +and the depth cures; wired in as `distributed_forward`. + +**Measured through the mind:** +- A 64-class forward pass: exact classifier 1.00, single-vector readout (K=1) 0.73, federated (K=8) 0.999 -- + federation moves the class wall, and at K=8 the federated pass tracks the exact classifier. In the Path D + sweep this is 16 classes faithful on one vector -> 96 on eight shards (~6x), the same federation that fixed + storage applied to the matmul. + +**Depth, the second question** (a deep net feeds each layer's noisy output into the next, so crosstalk can +COMPOUND), cured two ways, both wired: +- EXACT arithmetic per layer (`exact_matmul` / P1): no crosstalk to compound at all, so a deep integer forward + pass is exact at any depth (verified: a 2-layer integer net reproduces the float result exactly). The depth + decay was arithmetic crosstalk, not a depth wall. +- CLEANUP-GATING (`cleanup_books`): `softclean`, a soft dense-Hopfield, snaps each hidden activation back onto + the manifold of valid activations (keep the scale, denoise the direction), resetting crosstalk between layers. + The primitive is robust -- a crosstalk-corrupted activation goes from cos 0.78 to cos 1.00 with its clean + prototype. + +**KEPT NEGATIVE / honest scope:** federation buys FIDELITY / capacity, not fewer FLOPs -- total unbinds are +still C, grouped into K vectors; the parallelism is across the K shards, native on neuromorphic hardware. And +the end-to-end ACCURACY benefit of cleanup-gating needs a well-formed (trained) activation manifold, as in +exp_A1's trained MLP; with untrained or class-mean weights it is seed-dependent, so it is NOT asserted as an +always-win -- only the cleanup primitive (which robustly denoises onto the manifold) and the exact-arithmetic +depth cure (which is exact) are. The faculty delegates to the engine's own bundle/unbind, adding federation and +the depth cures, never a new algebra. + +Tests: +2 (a mind-level test -- federation moves the class wall, K=8 tracking the exact classifier; exact_matmul +per layer exact at depth; the softclean cleanup primitive denoising onto the manifold; the cleanup_books path +wired through the mind -- plus a CI selftest wrapper for the module). 739 -> 741. + +## Bucket A under federation -- selection, sequence, and the archive wired to the same lever (shipped) + +The last of the Path D advancements. The Bucket-A experiments re-opened three more single-vector walls under the +distributed premise, and all three are the SAME conservation law -- a superposed readout capped by per-vector +crosstalk, federated across K shards -- applied to different tasks. So they wire to the faculties that already +embody the federation move, not to new redundant ones. + +- **A3 hypothesis selection** and **A4 sequence memory** are the width faculty, federated. `superpose_compute` + gained a `shards=K` parameter: it spreads the items across K vectors (item i -> shard i mod K) and recovers + each shard separately, moving the width wall ~K-fold, plus a `decoded` output (per-item cleanup to a codebook) + for the sequence case. Measured through the mind: picking the planted match out of 160 candidates goes from + 0.38 (one vector) to 1.00 (K=8); recalling a 160-symbol sequence goes from 0.58 to 1.00. One call now serves + both -- pass a `query` to select, pass position-atom `keys` + a symbol `codebook` to recall a sequence. +- **A5 federated archive** is the storage array's federation applied to the CONTENT archive. `FederatedArchive` + (new in holographic_archive.py, wired as `federated_archive`) routes image i to shard i mod K over K aligned + HolographicArchive shards. Measured: at a FIXED total dimension, a monolithic archive and a 4-shard federated + one recover 64 images at the SAME quality (corr 0.965 vs 0.965) -- capacity federates (total = K x per-shard) + while recovery is conserved, the conservation law holding for images exactly as it did for symbols. +- **A2** (dense continuous matmul in superposition) is the FOIL, not a new capability -- it is the lossy bundle + `exact_matmul` (P1) replaces, and it stays on the record as the kept negative (it never gets good because it + has no cleanup; A3/A4 win precisely because they END in a discrete cleanup -- argmax / codebook snap). **A6** + (residue integer range) is `exact_matmul`'s own range federation over moduli, already shipped with P1. + +This closes the Path D integration: every advancement its experiments demonstrated is now a UnifiedMind faculty +or a measured property of one -- federation for storage (`storage_array`), width (`superpose_compute`, now with +shards), the archive (`federated_archive`), and the forward pass (`distributed_forward`); exact arithmetic +(`exact_matmul`); and sublinear lookup (`pivot_index`, `routed_recall`). The pure conservation-law measurements +(block federation, depth-vs-width, the factor wall on the existing resonator) remain evidence in the experiments, +not invented methods -- a faculty has to earn its place. + +Tests: +2 (a mind-level test that federating `superpose_compute` moves the selection AND sequence-length walls, +and one that the federated archive conserves recovery at fixed total dim while federating capacity). 741 -> 743. + +## Federation / conservation diagnostic -- the through-line as a callable readout (shipped) + +The honest way to wire the Path D conservation MEASUREMENTS into the mind -- rather than leave them only in the +experiments -- is as a diagnostic, the same family as `capacity_report` and `calibration_report`. (Forcing a +measurement into a capability faculty would be a fake faculty; a diagnostic whose job IS to measure and report +is the right shape.) `federation_report` operationalizes the 'as above, so below' law on the mind's own +dimension and kernel, delegating to `storage_array`: + +- `per_vector_budget` -- the largest single-shard load whose recall still clears the threshold (measured ~51 + symbols at D=1024, 0.90 recall: ~0.05 x D -- the figure depends on the threshold); +- `federated` -- a spot check that K aligned shards hold ~K x that budget at the same recall (4 shards -> 204 + symbols at 0.94); +- `conservation_ratio` -- partitioning the dimension in half holds total capacity (a half-D vector holds ~half + the budget, so two tie one full vector): measured 0.98, the block-federation finding that federation buys + capacity from more DIMENSIONS, not for free; +- `recommended_shards` -- ceil(target / per-vector budget), a planning readout (500 items -> 10 shards). + +This is the federation-aware companion to `capacity_report` (which charts a single vector's noise-wins cliff): +together they cover the per-vector cliff AND how federation moves it. It wraps `experiment_below_federation` +(conservation under partitioning) and `experiment_array_scale` (the per-vector budget and its scaling). The +other two conservation measurements are the SAME law in different costumes and are referenced here rather than +given redundant methods: `experiment_depth_vs_width` (escape the per-vector wall by recursion/DEPTH instead of +width -- the mind's `encode_tree`/`peel` are the depth half) and `experiment_factor_wall` (the factorization +search cliff vs dimension, measured on the resonator the mind already exposes via `decompose_structure`). + +Honest scope: the budget is the DISCRETE-symbol (cleanup-gated) ~0.05-0.1 x D regime; continuous compute with no +cleanup is the lower ~0.02 x D regime (see `distributed_forward`); and federation buys fidelity and capacity, +not fewer FLOPs. + +Tests: +1 (a mind-level test: the diagnostic measures a per-vector budget in the conservation-law range, K +shards hold ~K x it at recall, the partition-conservation ratio is ~1, and the shard recommendation matches +ceil(target / budget)). 743 -> 744. + +## Gradient-free substrate-native learning -- reservoir + prototype classifier wired as faculties (shipped) + +Translation (the RNS lever) made the substrate RUN trained networks exactly, but not TRAIN them. That gap is +closed with gradient-free learning methods the field already proved -- adopted rather than reinvented -- two of +them wired as UnifiedMind faculties on machinery the engine already had. + +`reservoir` (holographic_reservoir.HolographicESN) -- an Echo-State Network whose recurrence IS holostuff's +`permute` (a cyclic shift, hence norm-preserving / orthogonal = the echo-state property). The reservoir is +FIXED; only a linear readout is trained, by one closed-form ridge solve -- truly derivative-free and +deterministic. Diagnostic finding: the permutation recurrence EQUALS a classical random-matrix ESN on NARMA10 +(NRMSE 0.560 vs 0.562), so the engine's native operator is a real reservoir with no penalty. Tuned, NARMA10 +reaches a literature-grade NRMSE 0.367 +/- 0.001 over 5 seeds (1.59x over a linear-on-raw baseline; the +reservoir features carry it -- state-only equals state+input). It also LEARNS autoregressive text generation +(readout learned by ridge, the substrate generating from it). KEPT NEGATIVES: the first untuned cut was 1.18 +(worse than the mean) -- leak too low for NARMA's step-level dynamics, fixed by leak=1.0 + centered input; +chaotic free-running prediction diverges pointwise after ~one Lyapunov time (the climate is learnable, the +weather is not); periodic free-run tracking is loose; the readout learns a linear map of FIXED features. + +`prototype_classifier` (holographic_classifier.HolographicClassifier) -- the HDC/VSA learner. Encode each +example (bind a feature-id atom with a ScalarEncoder level, bundle over features), bundle a class's examples +into one prototype (a one-shot centroid), then perceptron retraining: on a miss, pull the correct prototype +toward the example and push the wrong one away (add/subtract on bundled vectors, no gradients). Measured (test +acc, 3 seeds): digits 0.902 -> 0.949, breast_cancer 0.934 -> 0.949, wine 0.981 (saturated) while raw +nearest-centroid is only 0.667 -- the encoding lifts the centroid model dramatically. KEPT NEGATIVE (the +field's own verdict): retraining beats the one-shot centroid, but the classifier lands just BELOW a tuned +linear model (logistic regression, by 0.006-0.016) -- traded for a dead-simple gradient-free rule. Kept +genuinely gradient-free (the perceptron rule), not the SGD-based methods that wear the HDC label. + +Both are the TRULY derivative-free corner. The local-gradient methods remain queued: Equilibrium Propagation on +the modern-Hopfield cleanup (free + nudged phases, contrastive-Hebbian, makes attractors learned), and +Forward-Forward / Mono-Forward (layer-local goodness, deeper nets). Standing caveat: native learning at +small/moderate scale, not a route to frontier scale. Real basis: Jaeger / Maass (reservoir computing); Kanerva +/ Rahimi / Imani / Kleyko / Hernandez-Cano (HDC prototypes, AdaptHD / OnlineHD); Scellier & Bengio (EP); Hinton +(Forward-Forward). + +Tests: +4 (reservoir selftest: fixed reservoir + ridge readout learns one-step prediction and is deterministic; +classifier selftest: one-shot + perceptron retraining beats chance, does not hurt, and is deterministic; plus +two integration tests running both faculties end-to-end through UnifiedMind). 744 -> 748. + +## Equilibrium Propagation -- the learning rule for the energy-based Hopfield cleanup (shipped) + +The reservoir and prototype classifier are TRULY derivative-free, but both only learn a linear map of fixed +features. Equilibrium Propagation (Scellier & Bengio, 2017) is the LOCAL-GRADIENT method that learns the +HIDDEN weights of an energy-based net, so it fits a NONLINEAR task they cannot -- and it is exactly the +learning rule for the energy-based (Hopfield) memory the engine uses as a FIXED cleanup (B1): where cleanup +relaxes a query to a stored attractor, EP learns the weights so the energy minima ENCODE a task. + +`holographic_equilibrium.EquilibriumNet` -- a 1-hidden-layer continuous Hopfield net (hard-sigmoid rho = +clip[0,1]; symmetric weights Wxh, Who by construction). No backprop. Relaxations of the same circuit: a FREE +phase (clamp the input, relax to an energy minimum = the prediction) and NUDGED phases (add +/- beta * +1/2||o - y||^2 to the energy, relax again). The weight update is the contrastive difference of the nudged +equilibria, dW ~ (1/2beta)(rho(s_-) (x) rho(s_-) - rho(s_+) (x) rho(s_+)), which estimates the loss gradient. +We use SYMMETRIC nudging (Laborieux 2021): the +beta and -beta pair cancels the leading O(beta) bias. + +VALIDATED honestly: +- Gradient correctness: the symmetric EP update matches the true gradient (central finite differences over the + free-phase loss) to COSINE 1.000 on a tiny net -- EP's defining property, measured, not assumed. +- Nonlinear learning: on two interleaving moons (noise 0.10) EP reaches ~0.92 test accuracy vs a linear + least-squares foil's ~0.85 -- the hidden layer earns its keep; a linear readout on fixed features (the + reservoir / classifier regime) cannot separate the moons. + +KEPT NEGATIVES (on the record): +- EP LANDS BELOW exact backprop: a tanh MLP trained by real backprop reaches ~1.00 on the same moons; EP's + ~0.92 is the cost of a biased finite-beta gradient estimate. EP is local-gradient, not a free lunch. +- It needs SYMMETRIC weights, and costs THREE relaxations per update (free + two nudged) -- far more compute + than the one-shot reservoir / classifier rules. +- Instability if pushed: large lr / weight-init drives the relaxation to collapse (~0.5, chance); the working + regime needs a converged free phase (longer t_free, smaller dt) and a modest lr. +- Two bugs found-and-fixed during the build, kept as lessons: (1) the hard-sigmoid derivative must be INCLUSIVE + on [0,1] -- else a state initialized at 0 has zero force and never moves; the gradient check caught it as + cosine 0.000. (2) The two-moons split must be SHUFFLED -- an index split left the test set single-class, + reading as acc 0.000; a harness bug, not an EP bug, caught only because below-chance accuracy is a red flag. + +Both the truly-derivative-free corner (reservoir, classifier) and now the local-gradient corner (EP) are +shipped; Forward-Forward / Mono-Forward (layer-local goodness, deeper nets) is the one method still queued. +Standing caveat: native learning at small / moderate scale, not frontier scale. Real basis: Scellier & Bengio +(2017); Laborieux et al. (2021). + +Tests: +2 (an EP selftest -- the symmetric update matches finite differences to cosine > 0.9 AND it learns two +moons past a linear foil, deterministically; plus an integration test running the EP faculty end-to-end +through UnifiedMind). 748 -> 750. + +## Forward-Forward -- backprop-free depth from local objectives, with a loud kept negative (shipped) + +Forward-Forward (Hinton 2022) is the last family and the DEPTH corner: it stacks many layers, each trained by +its OWN local objective with no gradient flowing between them -- depth without a global backward pass and +without EP's settling. Mechanism: replace backprop's forward+backward with TWO forward passes. POSITIVE data -> +train each layer to high "goodness" (mean squared activity); NEGATIVE data (the same input with a WRONG label +embedded) -> train each layer to low goodness. Each layer's local loss is a logistic on (goodness - theta); its +weights move by the gradient of THAT loss alone. Every layer L2-NORMALIZES its output before the next sees it, +so a later layer can't read the length an earlier one already separated. Classification is label-embedded: +prepend a one-hot label; at test try each label, forward, and pick the highest accumulated goodness. + +`holographic_forward.ForwardForwardNet`. Two implementation fixes were needed and are kept as lessons: (1) a +single global theta fails because layer goodness scales differ wildly (layer 0 ~1-4, layer 1 ~0.015 after +normalization) -- the fix is a PER-LAYER adaptive threshold (EMA of the goodness) so each logistic stays +centered and has gradient; (2) prediction must sum goodness over ALL layers (the constant 'a label is present' +part cancels across candidates, leaving each layer's learned label-vs-input compatibility). With those, the +mechanism works: on separable 4-class blobs it classifies at 100% with a positive-minus-negative goodness gap +of ~+2.4 on held-out data. + +KEPT NEGATIVE (MEASURED, loud -- the most humbling of the program): +- At the small scale tested this compact FF is a WORKING but WEAK classifier. It TRAILS a plain linear / + logistic model on EVERY task tried: two-moons ~0.88 (a tie with linear, NO nonlinear advantage -- unlike EP); + overlapping 4-class blobs 0.95 vs linear 0.99; sklearn digits (its natural high-dim habitat) 0.88 vs logistic + 0.97. It beats linear only on a radial task where linear PROVABLY fails (~0.69 vs 0.47), and even there weakly. +- FF's published accuracy (Hinton's ~1.4% MNIST error) needs the full-scale recipe -- many layers, large width, + long training, carefully built negatives -- not reachable in a compact CI-fast module. What this module + contributes is the MECHANISM (backprop-free, settling-free depth from local objectives), a conceptual route, + NOT a competitive number. +- Local-gradient, not derivative-free (like EP). Goodness-based label inference costs one forward pass per class + at test. Sensitive to the goodness threshold and the negative-data quality. +- The stronger Mono-Forward (2025) refinement (per-layer LOCAL supervised projections to logits) is reported to + match tuned backprop; it is the natural next step for a competitive FF-family accuracy and is NOT built here. + +This closes the four-family learning program. Honest summary of the whole arc: the TRULY derivative-free corner +(reservoir, prototype classifier) is competitive at small scale; the LOCAL-GRADIENT corner splits -- Equilibrium +Propagation genuinely learns nonlinear functions and beats linear (two-moons 0.92 vs 0.85), while Forward-Forward +demonstrates backprop-free depth but trails linear at this scale. None reaches frontier scale; the engine now +holds a clear-eyed MAP of what substrate-native learning buys and where each method's boundary lies. Real basis: +Hinton (2022); Mono-Forward (2025). + +Tests: +2 (an FF selftest -- the local-goodness mechanism classifies a separable task and separates positive +from negative goodness, deterministically; plus an integration test running the FF faculty end-to-end through +UnifiedMind). 750 -> 752. + +## NONLINEAR DYNAMICS COMPANION (shipped): learning a chaotic flow the linear propagator cannot + +This is the above/below examination's strongest "ABOVE" candidate, realised: the unlocked LEARNING aimed +straight at the most embarrassing kept negative in the dynamics line. `learn_dynamics` (Propagator) fits ONE +per-frequency complex transfer -- the linear Koopman/DMD operator. Exact for linearisable flow (it recovers +advection-diffusion almost perfectly), but a single fixed linear map cannot follow a state-dependent +nonlinearity: the record already carried "on a shock-forming Burgers field the linear propagator does WORSE +than persistence (0.054 vs 0.006; 0.125 vs 0.015)". The fix the negative itself named was "a learned lift". + +holographic_chaos.py / `mind.learn_chaos` is that lift, and it DELEGATES to the reservoir (holographic_reservoir) +rather than re-implementing a learner -- a fixed nonlinear echo-state expansion read out by a TRAINED linear +map learns the one-step evolution operator a linear transfer structurally cannot. NonlinearPropagator.learn +captures per-coordinate normalisation, fits the reservoir readout (one ridge solve) to map state(t) -> +state(t+1); `predict_sequence` gives one-step-ahead forecasts, `free_run` closed-loop rollout. lorenz_trajectory +(RK4) gives the selftest a known chaotic system with no external-data dependency. + +MEASURED (Lorenz '63, the canonical reservoir-computing test, RK4 dt=0.02): +- ONE-STEP is a clean WIN and NOT a strawman. Reservoir one-step ~0.0014 relative error vs the BEST linear map + (full DMD) ~0.059 and persistence ~0.071 -- ~40x better than best-linear, ~50x better than persistence. The + engine's own circulant propagator only ties persistence. A linear map sits at the chaos floor because the + Lorenz flow is state-dependent; the nonlinear reservoir genuinely learned the local evolution operator. +- Deterministic (same seed -> identical readout). Closed-loop free-run tracks the attractor ~10x longer than + persistence. + +KEPT NEGATIVES (loud, on the record -- the boundaries, established by sweeps, not guessed): +- Closed-loop horizon is only ~ONE Lyapunov time -- far short of the ~5 the one-step error implies. A 0.0014 + one-step error under clean chaotic growth would give ~5 Lyapunov times; getting ~1 means the AUTONOMOUS system + diverges faster than chaos alone: the well-known reservoir free-run STABILITY problem. State-noise helps only + marginally (noise=1e-2 is the sweet spot; 1e-1 shortens it to ~0.2), bigger reservoirs help modestly + (dim 500->1500 took 0.4->0.8), and -- the key finding -- the recurrence MIXING is NOT the lever: cyclic-shift, + random-permutation, AND an inline unitary-bind (random circulant orthogonal) recurrence all cap at ~0.4-0.9 + Lyapunov times. The wall is closed-loop stability, not mixing. Cracking it is a research problem of its own; + this module does not claim to. +- HIGH-DIMENSIONAL PDE FIELDS are out of reach for a single global reservoir. Forecasting a 48-D Burgers field + one-step lands ~0.27-0.35 relative error, far worse than persistence; Equilibrium Propagation as a memoryless + field regressor was also ~0.08 (worse than persistence). Pathak et al. (2018) forecast the chaotic + Kuramoto-Sivashinsky equation with LOCAL/parallel reservoirs precisely because one global readout cannot, and + EP's sweet spot is low-output (classification-shaped) targets, not 48-D field regression. The win above is a + genuine LOW-dimensional nonlinear-dynamics result, said plainly. +- On MILD dissipative Burgers the per-step change is tiny (persistence ~0.012), a punishing baseline with almost + nothing to win regardless of the learner -- which is why the clean win lives on chaos (weak persistence), not + on a smooth dissipative flow. + +As-above-so-below: this is the LEARNING (a within-/across-vector trained map) wired ONE RUNG UP to fix a +system-level dynamics negative, delegating to the reservoir faculty rather than re-building it -- the +examination's prediction made load-bearing by a cross-faculty test. Real basis: Jaeger echo-state networks; +Pathak et al. (2018) reservoir forecasting of spatiotemporal chaos; Lorenz (1963). + +Tests: +2 (a chaos selftest -- the nonlinear learner beats best-linear by >10x on the chaotic one-step map and +beats persistence's free-run, deterministically, without overclaiming the ~1-Lyapunov-time horizon; plus an +integration test running `learn_chaos` end-to-end through UnifiedMind). 752 -> 754. + +## Sparse cleanup readout + geometry-aware denoise -- match the MAP to the MANIFOLD (shipped) +A measured negative drove this: on a CONTINUOUS manifold (recovering UN-stored in-between points along a +photo-to-photo path in real SD latents), the softmax modern-Hopfield cleanup TIES or LOSES to plain +nearest-neighbour -- softmax 0.983, NN 0.998 -- because the dense blend weights in far codebook atoms and +OVER-SMOOTHS. This is the documented metastable-mixing / "fuzzy-memory" failure of the softmax update +(Ramsauer et al. 2020), and the field's 2024-25 fix is a SPARSE readout. + +Two VSA-native fixes, both prototyped and measured before wiring: +- SPARSE CLEANUP READOUT. `dense_cleanup(..., readout='sparsemax')` replaces the softmax blend with a + sparsemax simplex projection (Martins & Astudillo 2016) -- the Hopfield-Fenchel-Young move (Santos, + Niculae, McNamee, Martins 2024-25; sparse Hopfield Hu 2023 / Wu 2024) -- so the readout blends ONLY the + relevant patterns. Measured on the continuous SD-latent manifold: sparse 0.999 > NN 0.998 > softmax 0.983; + it REVERSES the softmax-loses-to-NN result. It does NOT regress discrete recall (still exact at high beta, + where sparsemax is also one-hot -- pinned by test). Default stays 'softmax', bit-for-bit unchanged. +- GEOMETRY-AWARE DENOISE SELECTOR. `denoise(method='geometry', samples=/codebook=)` reads the set's + `effective_rank` (the consolidation/SVD spectrum knee) and routes: LOW-rank-relative-to-count (a continuous + manifold) -> project onto that subspace (Milanfar's denoiser-as-manifold-map, RED 2017; a tensor-network + truncation in Stoudenmire's reading); HIGH-rank (distinct atoms) -> codebook recall. Measured: + manifold-projection recovers UN-stored in-between points at 1.000 vs the softmax blend's 0.983. + +KEPT NEGATIVES (travel with the methods): +- The sparse-beats-NN margin is THIN (~0.001, e.g. tour 0.996 vs 0.995); its CLEAR, robust win (every seed in + the variance harness) is over the softmax blend, not over NN. Reported, not oversold. +- Manifold projection only helps where the manifold is genuinely low-rank: forced onto the HIGH-rank distinct + photos it COLLAPSES recall to 67% (measured, and asserted by test). That failure is exactly why the router + reads the rank first -- match the map to the manifold, never project high-rank data. +- The softmax blend over-smooths continuous manifolds; use the sparse readout (or projection) there. + +The deepest VSA-native handle (recognised, not newly built): a continuous manifold should be HELD as a +function -- Vector Function Architecture / fractional power encoding (Frady, Kleyko, Kymn, Olshausen, Sommer, +Computing on Functions) -- which the RBF ScalarEncoder already is; the codebook-of-samples was the wrong +construction for a continuous quantity. The two wired items fix the readout and the routing; the functional +representation was in the encoder all along. + +As-above-so-below: the readout lives in the KERNEL (`dense_cleanup`, below) and is threaded UNCHANGED through +the mind's `denoise` (above) -- a test pins `mind.denoise(method='codebook', readout=...)` bit-for-bit to the +kernel call, so the mind delegates and does not re-implement. The geometry router delegates to `effective_rank` ++ `fit_manifold`/`manifold_denoise` + `codebook_denoise` (no new math), and a cross-faculty test proves it +picks projection on a low-rank manifold and recall on a high-rank set, with the high-rank-projection failure +kept as the guard. Real basis: Ramsauer et al. (2020); Martins & Astudillo (2016); Santos, Niculae, McNamee, +Martins (2024-25, Hopfield-Fenchel-Young); Hu (2023), Wu (2024); Romano, Elad, Milanfar (2017, RED); Frady, +Kleyko, Kymn, Olshausen, Sommer (Computing on Functions / VFA). + +Tests: +9 (sparsemax-simplex; softmax-readout-unchanged; high-beta-pins-to-hard-NN for both readouts; +sparse-beats-softmax and not-worse-than-NN on a continuous manifold; sparse-does-not-regress discrete recall; +effective_rank separates the geometries; the geometry router matches the right map AND projection-on-high-rank +is worse; the kernel<->mind above/below delegation; and a variance harness bootstrapping the margins across 12 +seeds). 754 -> 763. + +## Sparse readout in the SBC resonator -- the same fix one rung up, a measured capacity win (shipped) +The denoise finding generalised exactly as predicted: the SBC resonator's alternating projection +(`sbc_resonator`) updates each factor estimate with an annealed SOFTMAX blend over its codebook -- the same +dense blend whose metastable mixing hurt continuous-manifold cleanup. Swapping it for the sparse readout +(`readout='sparsemax'`, delegating to the shared `_sparsemax`) blends only the relevant atoms each step. + +MEASURED (F=3, B=16, L=16, all-factors-correct over 40 trials), softmax -> sparse: +- CLEAN CAPACITY rises sharply: N=25 0.47->0.62; N=50 0.00->0.12; N=80 0.00->0.25 -- sparse RECOVERS + factorizations where the softmax blend collapses to exactly zero. The sparse blend escapes the + metastable/limit-cycle traps that cap the softmax resonator at high alphabet. +- APPROXIMATE input (corrupted product blocks): helps at low corruption (clean 0.80->0.95) and TIES under + heavy corruption (corrupt=4 both 0.62). KEPT NEGATIVE: the win is largest on clean high-alphabet capacity; + heavy corruption is a tie, and absolute capacity is still modest (N=80 0.25, but vs softmax's 0.00). +- It NEVER regresses (sparse >= softmax in every cell). The annealed beta still drives explore->commit + (sparsemax keeps a sparse-but-broad set at low beta, one atom at high beta), so the search schedule holds. + +The CONFIDENCE null is matched to the readout. `_resonator_noise_null` / `resonator_confidence` now take the +readout and re-fit the procedure-matched noise floor under it (the cache key includes the readout) -- the +recurring rule: a null that does not match the actual procedure lies, and sparse manufactures a different +noise-floor agreement than softmax. Default stays `readout='softmax'`, bit-for-bit unchanged; sparsemax is +the measured-better opt-in (recommended, not yet the default, per the engine's backward-compatibility rule). + +As-above-so-below: the readout switch lives in the kernel resonator (`sbc_resonator`, below), threads +unchanged through `resonator_confidence` / `decompose_structure` and up through the mind's +`decompose_structure` and `factor_composite` (above) -- a test pins `mind.decompose_structure(readout=...)` +to the SBC factorizer's picks, so the mind delegates and does not re-implement. This is the SECOND faculty to +take the readout fix (cleanup was the first), confirming the panel's read: wherever a softmax blend appears, +the sparse readout is a candidate -- and here it cleared a real bar (a capacity win, not a thin margin). Real +basis: Frady et al. (2020), Kymn, Olshausen et al. (2024) resonator networks; Martins & Astudillo (2016) +sparsemax; Santos, Niculae, McNamee, Martins (2024-25, Hopfield-Fenchel-Young); Ramsauer et al. (2020). + +Tests: +5 (softmax-default-unchanged picks; the capacity win sparse>softmax at N=25/50 with no regression at +N=10; the confidence null is recomputed per readout; the mind delegates AND threads the readout through both +`decompose_structure` and `factor_composite`). 763 -> 768. + +## Sparse readout in the generative attractor -- the same fix cures generative mode collapse (shipped) +The third application of the readout finding, and the one that revealed a NEW axis. `generate` and +`generate_structure` are annealed cleanup attractors (denoise from pure noise, beta up / noise down); both run +through `dense_cleanup` (`generate_structure` slot-wise via `_structure_project`), so the sparse readout +threads in directly. The bar was "cleaner valid samples"; what it actually cleared was DIVERSITY. + +MEASURED (dim=1024-2048, recon-validity = cosine(z, reencode(decode(z))); diversity = fraction distinct combos +over the seeds): +- VALIDITY is a perfect tie: BOTH readouts produce structures that reencode their decoded combination at + cosine 1.000 in every config -- so the decoded combos are trustworthy and the structures are genuinely valid. +- DIVERSITY diverges sharply: softmax generation MODE-COLLAPSES (many random seeds settle into the SAME few + structures -- diversity as low as 0.03, i.e. one structure for 30 seeds, and typically 0.13-0.5), while + sparsemax stays diverse (0.6-1.0, nearly every seed a distinct valid structure). The mechanism is the SAME + metastable mixing: the softmax blend's wide blended basins funnel different noise starts to one attractor; + sparse's distinct basins let them settle into different valid structures. So the readout fix shows up here + as a cure for generative mode collapse, at NO validity cost. + +KEPT NEGATIVE: over a CONTINUOUS codebook, `generate` is UNAFFECTED by the readout -- both softmax and sparse +snap to a stored coarse atom (validity-to-manifold 1.000, novelty ~0). The old "bare codebook -> stored atom" +negative holds for both readouts; the sparse win is specific to `generate_structure` (the discrete composed +manifold), not to continuous-manifold generation. Pinned by a test. + +The creature is the boundary the analogy does NOT cross, and that is worth recording: `decide` ends in a HARD +argmax over scores, and the value readout it argmaxes is a clipped-cosine (ReLU-kernel) weighted average over +the top-k prototypes -- already sparse/thresholded, and a one-shot estimate, not an iterated soft-blended +attractor. There is no softmax blend there for sparsemax to improve, so the creature's decision path was left +unchanged (an honest non-fit, not a forced one). + +As-above-so-below: the readout switch lives in the kernel attractors (`generate`, `_structure_project`, +`generate_structure`, below) and threads up through the mind's `generate_vector` / `generate_structure` +(above); a test pins `mind.generate_structure(readout=...)` to the kernel generator's exact output, so the +mind delegates. This is the THIRD faculty to take the readout fix (cleanup -> resonator -> generator), and it +adds a new line to the unifier: a softmax blend in an ITERATED attractor is a sparse candidate not only for +accuracy/capacity but for SAMPLE DIVERSITY. Real basis: the cleanup attractor as diffusion (Ramsauer et al. +2020 modern Hopfield; Hopfield-Fenchel-Young, Santos/Niculae/Martins 2024-25); Martins & Astudillo (2016) +sparsemax; the VSA generate-by-denoising framing (Frady/Kymn/Olshausen/Sommer resonator + cleanup line). + +Tests: +5 (softmax-default-unchanged output for both generate and generate_structure; sparse structures stay +valid at recon-cosine ~1; the mode-collapse cure sparse-diversity > softmax-diversity; the continuous-generate +kept negative; the mind delegates AND threads the readout). 768 -> 773. + +## LEARNED ENERGY MEMORY (shipped): training the cleanup's attractors instead of storing them + +The panel's audit found the whole formal backlog (A1-A14, incl. tensor-train via tensor_bind rank) already +shipped; the ONE genuinely-unbuilt thing the seats' real methods converged on was that +holographic_equilibrium's docstring CLAIMED EP "is the rule that LEARNS those attractors" of the energy +memory -- but nothing actually trained a cleanup's energy. EP ran as a standalone classifier; the cleanup +stayed fixed (classical = snap to a stored atom; modern-Hopfield dense_cleanup = relax against a fixed +codebook). holographic_energy.py / `mind.learn_cleanup` makes good on that claim. It DELEGATES to +EquilibriumNet (not a new learner) to train a denoising AUTO-ASSOCIATOR -- (sample+noise -> sample) pairs, +hidden bottleneck ~ D/2 forcing the attractor set onto the low-dim manifold -- whose `cleanup(x)` clamps x, +relaxes, and reads the free-phase output: a noisy query PROJECTED onto a LEARNED manifold instead of snapped +to the nearest stored sample. torus_bump_manifold gives the selftest a known continuous nonlinear manifold +(a Gaussian bump at a continuous position on a latent_dim-torus; curved, NOT low-rank, so SVD/consolidation +can't denoise it and a finite codebook can only QUANTIZE the continuous position) with no external data. + +The result is GEOMETRIC -- when learning the energy beats storing the codebook is the whole point: +- vs the FIXED SOFT energy cleanup (dense_cleanup): unconditional win on a continuous manifold at every + codebook size (1-D EP ~0.33 vs soft 0.43-0.51; 2-D EP ~0.43 vs soft 0.45-0.56) -- the soft cleanup returns + a softmax MIXTURE that blurs on a continuum while the learned net projects. Apples-to-apples: learned energy + memory beats fixed energy memory. +- vs storing DATA (hard 1-NN codebook of RANDOM manifold samples) the win is DIMENSIONAL. On a 2-D manifold at + MATCHED MEMORY (EP weights 2*D*hidden vs an equal-byte codebook, K~=48): EP ~0.43 vs hard-1NN ~0.49-0.50. + Tiling a d-manifold with samples costs ~grid^d points (curse of dimensionality); a fixed-size learned + projector scales with the manifold's intrinsic structure, not its volume. Deterministic (Who allclose). + +KEPT NEGATIVES (loud -- they ARE the boundary, measured by sweeps): +- DISCRETE atoms are the wrong job. Queries = noisy versions of a finite stored set -> HARD 1-NN returns the + EXACT atom (~0.02-0.03) and is unbeatable; the learned approximate energy (~0.21) loses. This is B1's + "single-item identity is a tie" SHARPENED: against the hard cleanup it's a loss, not a tie. Use the existing + cleanup for discrete recall. +- In 1-D the curse does NOT bite, so a matched-memory random-sample codebook BEATS the learned energy (1-D + K=32 ~0.27 vs EP ~0.33). The advantage over storing data REQUIRES manifold dimension >= 2. In 1-D, just + store the samples. +- The win over a codebook is at MATCHED memory, not unbounded -- give the codebook 2-4x more bytes and it wins + even in 2-D (2-D K=100 ~0.41, K=200 ~0.35 vs EP 0.43). And EP inherits its weakness at very high output + dimension (this targets moderate D, low intrinsic-dim manifolds; it is NOT a high-D field denoiser -- cf. + the chaos module's 48-D Burgers negative). + +This is the LEARNING reaching the engine's most fundamental fixed object (the cleanup) -- the examination's +"below" unlock realised, the apex of the learning arc (reservoir -> classifier -> EP -> FF -> learn_chaos -> +learn_cleanup): the through-line was "make the fixed objects trainable", and the cleanup was the last and +deepest one. It is also the natural LEARNED prior for the Plug-and-Play/RED loop the engine already runs +(Milanfar: a denoiser is a map of the signal manifold -- now a LEARNED map). Real basis: Krotov-Hopfield / +Ramsauer et al. (the energy memory); Scellier-Bengio (2017) / Laborieux (2021) Equilibrium Propagation (the +local learning rule); Romano-Elad-Milanfar (RED) for the prior framing. + +Tests: +2 (an energy selftest -- the learned energy beats both the soft cleanup AND a matched-memory +random-sample codebook on a continuous 2-D manifold, deterministically, while the hard 1-NN cleanup wins on +discrete atoms (kept negative); plus an integration test running `learn_cleanup` end-to-end through +UnifiedMind and beating the fixed soft cleanup). 773 -> 775. + +## Grounded answering -- short, accurate, constructed sentences from retrieved knowledge (shipped) +The text-generation review's honest finding drove this: the engine's RELATIONAL layer answers questions +correctly and traceably (is_a chains, role lookups, learned-meaning similarity, classification), while its +GENERATIVE layer is locally fluent but globally incoherent (measured: longest verbatim run 3-8 words and +85-100% novel 4-grams, so NOT snippet-copying -- but no sentence-level meaning; and the structure verifier +rates a Markov walk as MORE typical than real text, so it cannot certify coherence). The right way to "answer +a question with a sentence that makes sense" is therefore NOT to generate -- it is to RETRIEVE the facts and +REALIZE them. + +`answer_text(question)` (faculty) = `realize_answer(answer(question))`. It delegates ALL retrieval to the +existing `answer()` router (which maps a question to the brain's real operations) and adds the one missing +piece: a surface-realization layer (`holographic_answer.realize_answer`) that builds a short sentence from the +retrieved STRUCTURE -- the is_a chain, the role value, the learned-meaning neighbours. Template-based NLG over +a holographic knowledge base, the standard pre-neural move, deliberately using the parts that WORK and NOT the +free n-gram walk. + +The three properties, MEASURED on a known encyclopedia + dictionary battery: +- ACCURATE: 11/11 on known questions. "Yes -- a dog is a mammal, which is an animal."; "No -- a salmon is a + fish, and ultimately an organism, not a bird." (correct no, and it explains what it IS); "The capital of + france is paris."; "A dog is a mammal -- more broadly, an organism. It's closely related to cat, wolf...". +- NO FABRICATION: 3/3 honest abstentions on unknowns. "I don't have dragon in my knowledge, so I can't say + whether it's an animal." -- the calibrated-abstention discipline applied to language: unknown concept, + low-confidence recall/classify, or a question that falls through to the generation path -> abstain, never + invent. Confidence/score floors gate role/recall/classify; an is_a "no" is only emitted when the subject is + actually known (chain length > 1), else it abstains. +- NOT VERBATIM: the sentence is CONSTRUCTED from the structure (a new sentence, not a copied source line); + verbatim only where the answer simply IS a stored value (a capital, a parent class) -- i.e. only when that + is what was asked for. Article (a/an) and natural-list rendering handle 1/2/3+-link chains gracefully. + +KEPT CAVEAT (loud): the "closely related to" neighbours come from the dictionary-meaning space, which groups +by shared DEFINITION words -- so they can include attributes ("four", "wood", "leaves"), not only clean +taxonomic siblings. The is_a/role parts are exact; the relatedness part is associative, consistent with the +meaning_predict finding that the dictionary space separates related words at d'~0.76 (good, not perfect). A +concept-only filter on the neighbours is the obvious next refinement. + +As-above-so-below: the realizer is a pure function over the `answer()` struct (fast deterministic unit tests +pin every branch); `answer_text` is exactly `realize_answer(answer(q))` (a test pins the delegation). Real +basis: template-based natural-language generation over a knowledge base (the standard grounded-QA architecture +before neural LMs); the engine's own relational faculties (climb/is_a/read_role/define/classify/recall) supply +the content; the calibrated-abstention thread supplies the "don't fabricate" floor. + +Tests: +10 (realizer form/accuracy per kind; low-confidence and unknown-subject abstention; recall/classify +score gates; completion/unknown abstain; helpers; and end-to-end: answer_text delegates to answer(), is +accurate, and does not fabricate on unknowns). 775 -> 785. + +## VSA-native question routing -- understand the question from a blend of word meanings (shipped) +The answerer's reach was limited by `answer()`'s brittle regex templates: natural or verbose phrasing +("could you tell me whether a dog is an animal", "do you happen to know the capital of japan") missed the +template and abstained even though the brain knew the answer. The fix is the engine's OWN machinery, exactly +as proposed: the text encoder already turns a string into a BUNDLE of its word meanings (the VSA blend), and +that bundle is a good INTENT signal, so route the question by encoding it (`mind.perceive`) and matching it to +per-intent prototypes (each the mean bundle of several example phrasings), then dispatch to the brain's real +operations. + +The crux, kept honest: bundling is COMMUTATIVE, so "is a dog an animal" and "is an animal a dog" blend to +nearly the same vector -- the blend gives the KIND of question but not WHICH concept is subject vs object. So +intent comes from the blend, and the ARGUMENTS come from a concept-scan: find the words the mind actually +KNOWS (its class labels + lexicon) and use their ORDER (first found = subject, last = ancestor) to assign the +roles the commutative bundle cannot. Intent-by-blend + arguments-by-order is the whole design; each half does +the job the other can't. + +MEASURED: +- INTENT routing: on natural/verbose phrasings the regex abstained on (0/8), the blend routed 7/8 correctly + (the miss is "what's a salmon" leaning IS_A because "a salmon" appears in the IS_A examples). +- END TO END through `answer_text`: 5/6 of those phrasings now answer correctly via the VSA fallback ("Yes -- + a dog is a mammal, which is an animal." from "could you tell me whether..."), and the DIRECTION case + "is an animal a dog" -> "No -- an animal is an organism, not a dog." is resolved by the order-scan, which the + blend alone cannot do. +- BACKWARD-COMPATIBLE: `answer()` tries the exact templates FIRST and calls the VSA router only when they miss, + so every templated question is byte-for-byte unchanged (tests pin that the `via` field is not 'vsa' there). +- ABSTENTION PRESERVED / NO FABRICATION: arguments must be concepts the mind knows; an unknown concept yields + no usable pair and the router returns None -> the honest abstention fires. KEY FIX found by measurement: the + define-fallback is restricted to DEFINE/SIMILAR intents -- describing the lone KNOWN concept of an IS_A + question whose SUBJECT is unknown ("is a dragon an animal", animal known, dragon not) would answer the WRONG + thing, so those abstain instead. Better to abstain than to mislead. + +KEPT NEGATIVES (loud): short questions with overlapping content words can confuse adjacent intents (mitigated +to abstention, not a wrong answer); classify/recall need an explicit text PAYLOAD not a concept, so they stay +with the templates (this router covers the relational intents is_a/role/define/similar); and the intent +prototypes come from a fixed example set, so a heavily padded wording can fall below the intent floor and +abstain rather than route. The router is the broad net; the templates remain the precise one. + +As-above-so-below: the router returns an `answer()`-style struct, so `realize_answer` and the abstention floors +apply unchanged -- the new path reuses the whole grounded-answer pipeline, it does not fork it. Real basis: +the VSA blend (bundle of word-meaning atoms) as a bag-of-words intent signal; the engine's own +perceive/encode (the one text encoder) and its relational faculties (is_a/climb/read_role/define) for content; +binding/order, not bundling, for argument roles (the standard VSA lesson that superposition is order-free). + +Tests: +7 (intent classification of natural phrasings; the order-based direction fix; natural role/define +answers; abstain-when-subject-unknown; answer_text answers natural phrasings via the fallback; templated +questions unchanged/backward-compatible; abstention preserved on natural unknowns). 785 -> 792. + +## Ordered lists -- recipes, directions, instructions: how well, measured (assessment + correction) +Asked how well the engine handles ordered lists, the honest answer is: well, and better than its own docstring +claimed. Ordered sequences live in `SequenceMemory` via PERMUTATION-positional encoding -- each step's atom is +rotated by its 1-based position and bundled into one vector (a scrambled order is near-orthogonal, cosine +~0.03). The mind exposes it as `learn_plan(name, steps)`, `step_at(name, i)`, `precedes(name, a, b)`, and +`validate_plan(name_or_steps, constraints)`. + +MEASURED (dim 2048): +- REAL lists -- an 8-step pancake recipe, 7-step driving directions, 6-step chair assembly -- recall in order + EXACTLY (8/8, 7/7, 6/6). precedes is correct both ways; validate_plan passes correct ordering rules and, on + an impossible rule, returns False naming the exact offending pair. +- CAPACITY is far past the old "~8" claim. Forced-choice step recall (which cleans up against the step list) + is 100% out to length 40, ~99.7% at 80, ~92.5% at 120 -- a graceful decline, not a hard cliff. Position + decoding (the harder token->slot direction, used by position_of/precedes) tracks it: 100% to ~40, ~99% at + 80, ~93% at 120. The "~8" docstrings were corrected to these measured numbers. +- ROBUST cleanup: even with 1000 competing distractor atoms in the vocabulary, step recall at length 12 stays + 100% -- the permutation signal is clean enough that the right token wins without needing a small candidate + set. (So the strength is the encoding, not a forced choice.) + +KEPT NEGATIVES (loud, pinned in tests): +- REPEATS are half-handled. A recurring step ("stir" at positions 1/3/5) recalls correctly at EVERY slot + (position -> element is position-indexed), but the INVERSE position_of (element -> position) is an argmax and + returns only ONE of the occurrences. An all-occurrences query would need to threshold the per-position + scores instead of taking the argmax. +- Each step is a WHOLE-STRING ATOM, so this is exact, order-faithful recall of stored step labels, not + generation or paraphrase: a reworded query step ("beat eggs" vs the stored "beat the eggs") is a different + atom and will not match. There is no fuzzy step matching yet (a natural place to reuse the learned-meaning + space or an edit-distance fallback), and the step CONTENTS are opaque to the order machinery -- it captures + the meaning that lives in the ORDER, not the meaning inside each step. + +Net: for directions/recipes/instructions at realistic lengths (a handful to a few dozen steps) the engine +stores them faithfully and answers position, "what is step i", precedence, and constraint-violation queries +exactly. The two real gaps are all-occurrences-for-repeats and fuzzy step matching. + +Tests: +3 (exact ordered recall + precedence + validation on a realistic recipe; capacity pinned exact at 20 +and >=88% at 120; the repeat recall/position_of-limit pinned as a kept negative). 792 -> 795. + +## Executable procedures: HoloMachine wired INTO the mind (de-silo; milestone 1 of 4) (shipped) +HoloMachine -- the stored-program VM whose opcodes ARE VSA operations (LOAD/BIND/BUNDLE/PERMUTE/CALL), +where a program is one hypervector and a function library is one vector callable by name -- had been kept +deliberately ADJACENT to UnifiedMind ("a program is just another value; leave the interpreter standalone"). +That was the silo. This milestone makes it a FACULTY: the mind owns a `_machine()` built at the mind's own +dim and seed (the same share-the-substrate move `_seq_mem` makes), so a procedure's accumulator is a vector +in the mind's OWN space and the format is deterministic. + +New faculties (all thin delegations to the machine, nothing re-implemented): +- `learn_procedure(name, program)` -- store a named executable ACC->ACC recipe in the library; composable + (a procedure may CALL procedures defined earlier). +- `run_procedure(name_or_program, init_acc=None)` -- execute and return (accumulator, trace). `init_acc` + seeds the accumulator with a vector from the mind's own space -- the bridge that makes a procedure an + operation ON the mind's data, not just on the machine's data atoms. +- `decode_step(name_or_program, i)` -- read instruction i back as (opcode, operand): the von Neumann + encoding means a stored procedure is DATA you can inspect, not only run. +- `procedure_to_recipe(program)` -- express a procedure as a typed B7 StructureRecipe, bit-exactly. + +Distinct from `learn_plan` on purpose: a plan stores an ordered list of opaque step LABELS ("beat the eggs"); +a PROCEDURE stores a recipe of real operations that DOES something. The two are the read/exec halves of the +same "ordered steps" idea on one substrate. + +MEASURED / as-above-so-below: the de-silo is proven LOAD-BEARING, not nominal -- a procedure run through the +mind is BIT-FOR-BIT identical to the same program through a bare HoloMachine at the same dim & seed +(np.array_equal on the accumulator), so the faculty truly delegates. Correctness holds (LOAD a; BIND b; +BUNDLE c == bundle(bind(a,b),c) at cosine 1.0); CALL-composition computes the right result through the mind; +decode_step reads instructions back exactly; and `realize(procedure_to_recipe(prog))` == `assemble(prog)` at +cosine 1.0. KEPT NEGATIVE (inherited, documented in the VM): instruction decode is a noisy cleanup whose +capacity scales with dim (~32 instructions at dim 1024, ~128 at 4096) before bundle crosstalk wins -- the +honest HRR capacity wall, not hidden; for realistic recipe lengths it is exact. Real basis: von Neumann +stored-program model expressed holographically (instructions and data in one vector space); HRR +bind/bundle/cleanup as the execution engine. + +This is milestone 1 of 4. Next, on this foundation: richer holostuff opcodes (CLEANUP/ENCODE/FACTOR/DENOISE +that call the mind's faculties), a goal-addressable procedure-memory faculty (recall WHICH recipe achieves a +goal), and recipe generation/completion (predict the next op). + +Tests: +7 (bit-identical delegation; real-VSA-op correctness; accumulator seeded from a mind vector; +CALL-composition through the mind; decode_step as data; bit-exact procedure->recipe bridge; unknown-procedure +guard). 795 -> 802. + +## Richer opcodes: APPLY -- a procedure invokes the engine's faculties as steps (milestone 2/4) (shipped) +Milestone 1 wired the VM into the mind but its opcodes were still pure kernel algebra (LOAD/BIND/BUNDLE/ +PERMUTE). This milestone lets a procedure call the engine's higher faculties as steps, via ONE general, +extensible opcode rather than a opcode-per-faculty sprawl: + + APPLY means ACC := faculty(ACC) + +The VM gains the opcode and a faculty-name operand codebook (`fac_atoms`, alongside the data and function +codebooks); `run(..., handlers=...)` takes a host-supplied dict {faculty_name -> unary acc->acc map}. The +bare VM has no handlers, so APPLY is a SAFE NO-OP there -- a program with APPLY still assembles, decodes, and +runs everywhere. The mind supplies the handlers (`_procedure_handlers`), each delegating to a real faculty: +- `cleanup` -> the dense associative cleanup (`hopfield.dense_cleanup`) against the procedure's value-atom + codebook: relax the accumulator toward the nearest known value. +- `denoise` -> the mind's general manifold denoiser. + +MEASURED: APPLY cleanup is a real capability a plain list of kernel ops cannot match -- a procedure +SELF-CORRECTS a noisy accumulator. Seeding the accumulator with a heavily corrupted value atom (cosine-to- +truth ~0.07 at sigma 0.5, dim 1024) and running `[APPLY cleanup; HALT]` recovers it to ~0.79 (and to ~1.0 at +the tour's larger dim). Backward-compat is exact (a procedure without APPLY is byte-for-byte identical to a +bare HoloMachine -- the run() signature change is safe), the bare VM runs APPLY programs as a no-op, +decode_step reads APPLY back as data, and a procedure containing APPLY is still a typed B7 structure +reproduced bit-exactly. + +KEPT NEGATIVES / honest scope: `denoise` helps only when the accumulator carries low-rank/self-similar +structure; on bare random value atoms there is no manifold, so `cleanup` is the operative denoiser there. +And APPLY is deliberately limited to UNARY acc->acc maps -- the opcodes Moose floated that do NOT fit this +shape are out of scope: FACTOR/RESONATE produce MULTIPLE outputs (no single accumulator to write back), and +value-ENCODE needs a value in the mind's space rather than a faculty applied to the accumulator. APPLY is the +extension point if a unary form of any of those is later wanted (register it in `_procedure_handlers` and +`DEFAULT_FACULTIES`). + +As-above-so-below: the opcode lives in the VM (so programs stay self-contained and decodable), but the +SEMANTICS come from the mind's faculties through the handler hook -- the same delegation the whole de-silo is +built on. Real basis: dense associative memory / modern Hopfield cleanup (Ramsauer et al. 2020) as the +recover-toward-the-codebook step; the von Neumann stored-program model extended with a host-call instruction. + +Tests: +4 (APPLY cleanup recovers a noisy accumulator; backward-compat bit-identical; APPLY decodes and the +bare VM runs it as a no-op; APPLY procedure->recipe stays bit-exact). 802 -> 806. + +## Procedure memory: goal-addressable recall over the library (milestone 3/4) (shipped) +With procedures stored as data in one library vector, the question is whether you can recall the RIGHT one by +what it ACCOMPLISHES -- something a plain list of callables cannot do without manual bookkeeping. Two faculties: +- `recall_procedure(input_vec, output_vec)` -- given ONE (input -> output) example, return (name, score) of + the stored procedure whose behaviour best reproduces it. +- `recall_and_apply(input_vec, output_vec, new_input)` -- recall that procedure, then run it on NEW input: + learn an operation from one example, then reuse it (analogy/transfer, VSA-native). + +MEASURED (dim 1024): over a MIXED library (bind-b/c/d, permute, bundle-e), behavioural recall identified the +right procedure from a single example 100% of the time, and recall-and-apply transferred the recalled +transform to fresh input correctly 100% of the time. The elegant special case also holds: for a single-bind +transform the operation can be recovered ALGEBRAICALLY in O(1) -- unbind the input from the output and clean +the result against the transforms' keys -- 100% identification with NO candidate runs. + +HONEST COST / boundary: the general faculty is BEHAVIOURAL -- it runs each candidate on the input and matches +the output, so it is O(library size) in executions. That is the honest price of recalling an ARBITRARY +procedure by goal: the VSA encoding does not magically avoid running the candidates for general programs. The +O(1) algebraic shortcut is real but transform-specific (bind-parameterised transforms only), so it is shown in +the tests rather than wired as the default. A precomputed behavioural FINGERPRINT (run each procedure once on a +fixed probe, then match goals against the stored fingerprints) would make per-query recall execution-free and +even sublinear via the HoloForest -- a natural next step, noted but not built, and exact only for transforms a +fixed probe characterises (linear ones). + +As-above-so-below: recall_and_apply composes the M1 run faculty (executes through the same VM, with the M2 +APPLY handlers available), so it is the engine's content-addressable-recall competence pointed at PROCEDURES, +reusing the procedure machinery rather than a parallel store. Real basis: matched-filter / behavioural +identification; HRR unbind+cleanup for the algebraic special case. + +Tests: +3 (recall the right procedure from one example over a mixed library; recall-and-apply transfers to new +input; empty-library recall returns None). 806 -> 809. + +## Recipe completion: predict the next opcode from a partial recipe (milestone 4/4) (shipped) +The last upgrade closes the loop from running and recalling procedures to GENERATING them: given a partial +recipe, predict the likely next opcode. Two faculties: +- `learn_recipe_grammar(recipes, order=2)` -- learn the opcode-sequence statistics of a set of valid recipes + (only the opcode stream, i.e. the control SHAPE), into a dedicated token-level predictive model kept + separate from the mind's prose predictor. +- `complete_procedure(partial)` -- predict (opcode, confidence) for the next step of a partial recipe; an + empty partial predicts the typical FIRST opcode. + +Delegates to the existing PredictiveMemory (token-level n-gram with error-gated writes) rather than a new +predictor -- opcodes are just its symbols. MEASURED on a grammar (LOAD, BIND, then 0-2 of +{BUNDLE,APPLY,PERMUTE}, then HALT): it learned the HARD constraints -- after LOAD it predicts BIND at +confidence 1.0, and after two middle ops it predicts HALT -- and 100% of next-opcode predictions on held-out +partial recipes were grammar-VALID continuations. + +KEPT NEGATIVES: it is an n-gram over opcodes, so it predicts by the frequency of transitions it has SEEN -- +a recipe shape absent from training is not anticipated well; it predicts the single most-likely next opcode +(the model's soft mode gives a blended estimate); and it learns the opcode SHAPE, not the operands (predicting +the right argument is a larger-vocabulary problem left for later). The degenerate empty-context prediction +returns the right first opcode but at confidence 0 (the zero context vector), which is correct if unsmooth. + +As-above-so-below: the grammar is a thin token-level wrapper over PredictiveMemory, so recipe generation +reuses the same predictive-coding machinery the mind uses for sequences -- one predictor design, pointed at +opcodes. Real basis: n-gram / predictive-coding next-symbol modelling. + +This completes the 4-milestone procedure arc: M1 de-siloed the VM into the mind, M2 let procedures call the +mind's faculties (APPLY), M3 made the library goal-addressable (recall by example), M4 makes recipes +predictable. Tests: +2 (grammar predicts valid next opcodes incl. the hard LOAD->BIND and the HALT cap; +no-grammar returns None). 809 -> 811. + +## Fingerprint fast-path for procedure recall (milestone 5/4 -- the natural follow-on) (shipped) +M3 left an honest cost: behavioural recall runs EVERY candidate through the VM (O(library) executions per +query). This milestone removes that cost for the case it can, measured first. Two pieces: +- `index_procedures()` -- run each procedure ONCE on a canonical probe and cache the output (a behavioural + FINGERPRINT). One-time O(library) cost, amortised across all later recalls. +- `recall_procedure(..., method=...)` -- 'fingerprint' recovers the transform's kernel from the single + example and matches the IMPLIED fingerprint with ZERO program runs; 'behavioral' is the M3 scan; 'auto' + (default) tries the shortcut, trusts it only when its match clears `fp_floor`, and otherwise falls back. + +MEASURED (dim 1024): the shortcut is EXACT (confidence ~1.00) for the LINEAR / convolution class, and that +class is larger than expected -- it is bind AND permute and their compositions. The reason is a clean +identity: permutation is convolution by a shifted delta, so it commutes with binding exactly as a key does, +giving bind(P, unbind(permute(X), X)) == permute(P). Across a mixed library (binds, permute, additive bundle, +and a nonlinear cleanup procedure), 'auto' identified the right procedure 100% of the time -- linear ones via +the zero-run shortcut, the rest via the fallback -- and ran ~30x faster than the behavioural scan on a +bind/permute workload (8 ms vs 222 ms for 80 queries). Backward-compatible: with no index, 'auto' IS the +behavioural scan (100% unchanged). + +KEPT NEGATIVES / measured boundary: the fingerprint is reliable only for the convolution class. An ADDITIVE +transform (BUNDLE, i.e. x+c) lands borderline (~0.48) because the constant part partially aligns with the +probe, and a genuinely NONLINEAR procedure (one with an APPLY cleanup/denoise step) scores near zero (~0.01). +Both sit below the 0.5 gate, so 'auto' correctly routes them to the behavioural fallback -- the gate is what +makes the speed-up SAFE rather than a source of silent wrong answers. The confidence separation is wide +(1.00 for exact matches vs <=0.48 for everything else), so the gate is robust. One more measured caveat: the +exactness assumes the QUERY INPUT is unitary (so the unbind that recovers the kernel is clean); for a non- +unitary input the recovered kernel is noisier (~0.67 confidence in the tour) -- still well above the gate, so +recall still succeeds, just with less margin. + +As-above-so-below: index_procedures runs through the SAME run_procedure faculty (M1) with the SAME APPLY +handlers available (M2), and the fast-path is just the engine's unbind+cleanup competence applied to a +precomputed library -- no parallel machinery. The fingerprints could be dropped into a HoloForest to make +recall sublinear in library size as well (noted, not built; exact only for the linear class a fixed probe +characterises). Real basis: convolution-algebra / transfer-function identification (the kernel of a +shift-invariant linear map is recoverable from one input-output pair); HRR unbind+cleanup. + +Tests: +2 (fingerprint 'auto' matches behavioural across a mixed library and is backward-compatible without an +index; the shortcut is exact for bind AND permute and gated below 0.5 for a nonlinear procedure). 811 -> 813. + +## Procedure synthesis: CONSTRUCT a procedure for a goal (milestone 6) (shipped) +recall_procedure (M3) finds a procedure already in the library; this milestone builds the missing constructive +counterpart -- `synthesize_procedure(input_vec, output_vec, max_depth=2)` SEARCHES for a short program that maps +input -> output, even when none is stored. It runs a bounded breadth-first search over the VM's operations +(BIND/BUNDLE/PERMUTE x the data atoms), returns the SHORTEST program (as (opcode, operand) pairs ending in +HALT) whose execution reaches the target, and VERIFIES it by running it before returning. + +MEASURED (dim 1024): it constructs correct programs for single-op and composite goals -- bind, bind-then-bind, +and the order-SENSITIVE permute-then-bundle (it picks the right order; binding two atoms it may pick either +order, which is fine because binding commutes) -- each verified to map X -> target. Crucially the synthesized +program GENERALISES: run on a fresh input it performs the same operation (cosine >0.99 to the transform's +truth on the new input), so it captures the TRANSFORM, not the example pair -- the structured moves it searches +are what make one example enough. An unreachable target returns None honestly; a depth-3 composite is found +when max_depth=3 is allowed. + +KEPT NEGATIVES: the search branches by (ops x operands) per step, so it is EXPONENTIAL in depth -- practical +only for short programs (depth 2-3); it constructs programs only over the KNOWN operations and operands (it +cannot invent a new atom or a nonlinear step); and it may return an EQUIVALENT program rather than a unique +'intended' one. This is the panel's search theme (Baker's landscape search, the flow solver) on the program +space itself: a deterministic, verified pre-screen, with the honest exponential wall stated rather than hidden. + +As-above-so-below: synthesis applies the SAME kernel ops the VM executes and VERIFIES through the SAME +run_procedure faculty (M1), so a synthesized program is immediately runnable, decodable (M1), recallable (M3), +and reducible to a typed B7 structure -- it drops straight into everything already built. Real basis: bounded +program search / enumerative program synthesis, verified by execution. + +Tests: +3 (synthesize single + composite + order-sensitive programs, all verified; a synthesized program +generalises to a new input; an unreachable target returns None). 813 -> 816. + +## Control flow: IFMATCH (conditional) and ITERATE (the fixed-point loop) (milestone 7) (shipped) +Until now the VM had only straight-line execution plus CALL (subroutines) and HALT -- no conditionals, no +loops. So the one pattern that drives most of the engine -- input -> process -> feed the result back as input +-> repeat until the desired output -- could not be written as a PROGRAM, even though the engine runs exactly +that loop inside cleanup, the resonator, denoise, and the diffusion sampler. This milestone adds the two +missing primitives, both reusing the existing machinery: + +- `IFMATCH x` -- execute the NEXT instruction only if cosine(ACC, x) >= branch_tol, else skip it (a one- + instruction conditional; pair it with CALL for an if-then). Implemented by giving run() an explicit program + counter so a branch can skip forward. +- `ITERATE f` -- re-apply library function f to ACC until it CONVERGES (cosine to the previous ACC >= + converge_tol, a fixed point), OR a host `stop(acc)` predicate marks the desired OUTPUT reached, OR max_loop + is hit. The loop body is a named library function, so ITERATE reuses CALL's library-pull. Its trace entry is + the 4-tuple (op, f, iterations, reason) where reason is 'converged' / 'goal' / 'maxloop' -- the loop tells + you why it stopped and after how many passes (the "benchmark the result before exiting" visibility). + +MEASURED (dim 1024): ITERATE of a one-step cleanup body on a noisy accumulator IS the fixed-point loop -- +at low noise (sigma 0.3) it converges to the clean atom (cosine 0.11 -> 1.00) in ~2 iterations, reason +'converged'; the goal predicate exits the instant the output crosses the target (reason 'goal'); and a non- +converging body (a PERMUTE that rotates forever) correctly hits the cap (reason 'maxloop'). IFMATCH branches +both ways: the guarded CALL runs on a match (ACC -> bind(a,b)) and is skipped on a mismatch (ACC stays a), +with the trace showing exactly which path was taken. Backward-compatible: a program with no control flow is +byte-for-byte identical to a bare VM; IFMATCH (data operand) is a typed B7 structure bit-exactly, while +ITERATE (runtime library lookup, like CALL) is out of scope for the recipe bridge. + +KEPT NEGATIVES: ITERATE converges to a FIXED POINT, which is the clean atom only when the input is inside its +basin of attraction -- at higher noise (sigma 0.5, 0.7) the loop still converges in ~2 iterations but to a +partial recovery (cosine 0.81, 0.68), not the exact atom (the cleanup's basin shrinks with noise; an honest +property of attractor dynamics, not a bug). IFMATCH is a forward-only skip of ONE instruction (no backward +jumps), so it expresses if-then, not arbitrary goto; there is no general counted FOR loop (convergence/goal/cap +cover the AI case, and a count-as-operand would be awkward in the atom codebook) -- noted as the honest scope. + +As-above-so-below: the loop body and the conditional run through the SAME run()/CALL/APPLY machinery, so an +ITERATE can drive a procedure that itself uses APPLY cleanup/denoise (M2), CALLs sub-procedures (M1), or was +synthesized (M6) -- control flow composes with everything already built. This is the engine's own fixed-point +nature (Hopfield/resonator/denoise all iterate to attractors) finally expressible at the program level. Real +basis: fixed-point iteration / attractor dynamics; the von Neumann stored-program model with conditional and +loop control. + +Tests: +4 (ITERATE converges to the fixed point; goal and cap exits; IFMATCH branches both ways; control flow +is backward-compatible and the recipe bridge accepts IFMATCH but rejects ITERATE). 816 -> 820. + +## matmul in the loop: exact_matmul as an APPLY faculty (backlog VM-1) (shipped) +The control-flow milestone gave the VM a fixed-point loop; this gives the loop a real LINEAR-ALGEBRA step. +`set_matmul(W)` configures a matrix and `APPLY matmul` then does ACC := W @ ACC, carried by the engine's +EXACT RNS matmul (residue-number-system phasor multiply-accumulate -- no crosstalk). With a dim x dim W the +accumulator keeps its shape, so `ITERATE [APPLY matmul]` is a recurrent linear map iterated to a fixed point +-- the literal input -> process-by-a-matrix -> feed-back pattern, the shape of so much of AI. + +MEASURED (the marquee demo): a column-stochastic transition matrix iterated by `ITERATE [APPLY matmul]` IS +power iteration -- it converges to the matrix's STATIONARY DISTRIBUTION (the dominant eigenvector, lambda=1). +On a 64-state chain it reached the stationary distribution at cosine 0.9993 in 3 iterations (reason +'converged'), a real iterative algorithm expressed entirely as a VM program. Disabling it (`set_matmul(None)`) +makes APPLY matmul a safe no-op, so the opcode is harmless until configured; the bare VM is unaffected. + +KEPT NEGATIVES / scope: the matmul is EXACT for integer / fixed-point operands within range; a float matrix +and vector are fixed-point QUANTISED first, so the only error is that rounding (set by the scale), NOT the +crosstalk wall -- on large-magnitude operands (standard-normal values ~+-3) the default-scale rounding is +visible (~0.12 abs error on a raw matmul), while on well-scaled data like probability distributions it is +negligible (hence the 0.9993 convergence). One configured matrix at a time, and this step treats ACC as a raw +vector -- a deliberate, honest departure from the VSA algebra to do ordinary linear algebra inside the loop. + +As-above-so-below: the matmul handler is just another entry in the same APPLY registry as cleanup/denoise +(M2), so it composes with everything -- a loop body can matmul then clean, a conditional can gate a matmul, a +synthesized program could include one. The RNS matmul Moose added becomes the process step in the AI loop the +control-flow milestone made expressible. Real basis: power iteration / Perron-Frobenius (stochastic matrix -> +stationary distribution); Residue Number System exact integer matmul. + +Tests: +2 (ITERATE [APPLY matmul] converges to the stationary distribution; disabled matmul is a no-op). +820 -> 822. + +## Counted loop: REPEAT n runs the next CALL n times (backlog VM-2) (shipped) +ITERATE is a convergence/goal WHILE loop; this adds the counted FOR loop the VM was missing. `REPEAT n` runs +the FOLLOWING instruction n times -- expected to be a CALL, so the body is a named library function (any block +of work). The count is a small-integer operand drawn from a dedicated codebook (cnt:1 .. cnt:COUNT_MAX, default +8), mirroring how IFMATCH gates the next instruction; REPEAT consumes the CALL that follows it. + +MEASURED: REPEAT n; CALL shiftone (a one-PERMUTE body) yields permute(X, n) exactly for n = 1, 3, 5 -- an +exact, countable proof the loop ran the right number of times -- with the trace showing [REPEAT, CALL]. It +decodes back as (REPEAT, count) data and runs on the bare VM. KEPT NEGATIVES / scope: the count is bounded to +the count-atom set (1..8 by default, raise COUNT_MAX to extend); REPEAT repeats exactly ONE following +instruction, which must be a CALL (wrap any multi-op body in a function) -- if the next instruction is not a +CALL, REPEAT is a safe no-op and the next instruction runs once. Like the other control flow, REPEAT is runtime +(it consumes a runtime CALL), so the structural recipe bridge declines it alongside CALL/ITERATE. + +As-above-so-below: REPEAT reuses CALL's library-pull and threads the same handlers/loop knobs through the +recursion, so a REPEATed body can APPLY faculties, ITERATE, or CALL further -- it composes with the rest of the +control flow. The VM now has both loop kinds: ITERATE (loop until a fixed point / goal) and REPEAT (loop a +fixed number of times). Real basis: the counted-loop / bounded-iteration control primitive. + +Tests: +2 (REPEAT runs the next CALL n times, exact via permute; REPEAT decodes and runs on the bare VM). +822 -> 824. + +## Control flow composes: nesting + a worked program (backlog VM-3) (shipped) +Control flow is only real if it nests, so this validates that and ships a worked program as the proof. MEASURED +compositions all reach the right result: a counted loop of convergence loops (REPEAT 2; CALL refine, where +refine ITERATEs -- REPEAT>CALL>ITERATE>CALL>APPLY) denoises to the clean atom; a convergence loop whose body +CALLs (ITERATE double_clean -- ITERATE>CALL) converges; and runs are bit-identical run-to-run (determinism +holds through the nested control flow). The depth guard caps recursion; ITERATE's own iterations do not consume +depth (they reuse one level), so loops nest freely within the guard. + +The WORKED PROGRAM is a complete little routine in one procedure: `ITERATE clean_step; IFMATCH c; CALL tag; +HALT` -- denoise the input to a clean atom, branch on the cleaned result, and tag it only if it is 'c'. On an +input that cleans to c it runs [ITERATE, IFMATCH, CALL] and the accumulator becomes bind(c, tag); on an input +that cleans to d it runs [ITERATE, IFMATCH] and skips the tag. Loop + conditional + call working together on +the one substrate -- the proof that the VM is now a real little language. (A learning from the build, kept: a +conditional must come AFTER the denoise, not before -- raw noise at high dimension has cosine ~0.15 to the true +atom, below the IFMATCH gate, so the natural and correct order is process-then-branch.) + +As-above-so-below: the worked program uses ITERATE (M7), IFMATCH (M7), CALL (M1) and APPLY cleanup (M2) in one +assembled vector, run through the one VM -- every control and faculty primitive composing in a single program. +Real basis: structured-program composition; the denoise-then-classify routine is the engine's own recall +pipeline expressed at the program level. + +Tests: +2 (nested control flow composes and is deterministic; the worked denoise->classify->tag program runs +both branches correctly). 824 -> 826. + +## Automatic data-analysis pipeline as a VSA program (PIPE-1, shipped) + +The reason the VM grew control flow: a real, useful program that loops a process until it converges and +branches on the result. Handed a 1-D signal, `run_analysis_pipeline` runs ONE HoloMachine program -- not +Python control flow -- + + APPLY analyze ; ITERATE _denoise_step ; APPLY decompose ; IFMATCH structured ; CALL _train_validate ; APPLY save ; HALT + +-- where each APPLY delegates to a real faculty and the looping/branching is the recent VM (ITERATE/IFMATCH/ +CALL). The accumulator carries the signal through the denoise loop; then decompose hands the program back a +`structured` or `noise` FLAG atom, and the IFMATCH branches on it -- the data decides the path. + +Measured on a structured signal (1 + 2t + 3t^2 + noise, 256 points): analyze reports a line topology, the +ITERATE denoise loop settles the signal, decompose finds the 2-term quadratic law at explained variance +0.998, the IFMATCH fires, the CALL'd train+validate confirms the law extrapolates to the unseen last 20% +(held-out error 0.10 of the signal's std), and save stores the 256-point signal as a 157-byte generative law +(decompose's compression_ratio 6.5x). On PURE NOISE the same program denoises, decompose finds nothing +(explained variance 0.0, zero terms), the IFMATCH SKIPS the CALL -- train and validate never run -- and save +reports raw_only. Both branches, one program, driven only by what the data turns out to be (executed opcodes +APPLY/ITERATE/APPLY/IFMATCH/CALL/APPLY with structure present vs APPLY/ITERATE/APPLY/IFMATCH/APPLY without). + +The denoise loop body needed its own fix. A denoiser is a map of a manifold and a lone signal has none -- +denoise(auto) on a bare vector correctly refuses ("no free lunch"). So the prior is built FROM the signal: +its sliding windows form a trajectory (Hankel) matrix that is LOW-RANK for any smooth/structured signal +(classic SSA/Cadzow), so projecting those windows onto their own dominant subspace removes the noise. +`_denoise_signal` delegates that projection to denoise(method='adaptive') and reconstructs by anti-diagonal +averaging; it cuts noise ~3.4x on a structured signal and is ~idempotent (cosine 0.9999 under a second pass), +so the ITERATE settles in a couple of steps instead of spinning to max_loop. + +Kept negatives (inherited, surfaced not hidden): decompose_signal fits line-domain elementary laws +(polynomial, exponential) and harmonic laws well and reports NO structure on noise -- a clean branch +discriminator -- but it is NOT a universal fitter: a bare sine on a LINE domain is detected as "line" rather +than "ring" and missed (zero terms), so a purely periodic signal with no trend can slip through as if +structureless. This is the SINGLE-LEVEL pipeline: it finds the dominant law and a residual but does not yet +recurse into the residual; peeling structure layer by layer ("every level") is the natural follow-on (the B8 +peel module is exactly that engine). The accumulator's role changes mid-program (signal -> flag atom) -- a +deliberate state transition, not a type error, since each cosine compares same-length vectors. + +As above, so below: the pipeline's decompose records the same n_terms a direct decompose_signal call returns +on the final denoised signal, and the program runs through the same run_procedure/HoloMachine path as every +other procedure -- a custom hand-written program of (opcode, operand) tuples executes identically. The faculty +orchestrates, it does not fork. Real basis: Milanfar's denoiser-as-manifold-map (the adaptive projection), +Broomhead-King / Cadzow SSA (the low-rank trajectory prior), and the engine's own decompose_signal +(topology -> matched basis -> MDL-gated law). + +Tests: +5 (826 -> 831), in test_procedure_faculty.py. + +## Recursive peel: accessing structure on every level (PIPE-1 follow-on, shipped) + +PIPE-1's single decompose finds the dominant law and a residual but stops -- "every level" was the part it +left open. recursive=True turns the single `APPLY decompose` into `ITERATE _peel_step`: decompose the +dominant law, peel its residual, decompose THAT, layer by layer, until nothing structured remains. The loop +is the recent VM's ITERATE; it converges on the engine's OWN MDL verdict -- decompose returns n_terms==0 when +its gate admits no term (the same gate that returns 0 on pure noise) -- so peeling stops exactly when the +residual is noise. An `APPLY assess` then records the ladder and flags the train/save branch. + +The stop criterion matters and was measured. The first try gated each level on "did it explain >= 30% of the +residual?" -- and that FAILED on the very case the peel exists for: a line trend UNDER a comparable sine. The +trend explains only ~0.3 of the variance noiseless, and less with noise, so the floor rejected the real first +level and peeling never started (0 levels on a noisy trend+sine). The fix is to trust decompose's MDL gate, +not the explained fraction: a level is REAL if the gate admitted a term (n_terms >= 1), however modest its +share. A level can be real and small. + +Measured: a noisy trend+sine (0.5 + 2t + sin(2*pi*5t) + 0.2 noise) peels into 3 levels -- line(trend) -> +mobius(periodic) -> line(cleanup) -- cumulative explained 0.997, residual down to 0.04, where a SINGLE +decompose explains only ~0.29 (it fits the trend but is thrown by the sine, or vice versa -- not both at +once). poly+exp is captured in ONE level (its additive dictionary fits both at once) and peeling correctly +stops at one -- it does not invent layers. Pure noise yields zero levels (raw_only, training skipped); a hard +mix (a trend + two sines of different frequency) ALSO yields zero -- detect_topology sees "line" and the MDL +fit admits no term against the strong oscillations, an honest inherited limit of decompose_signal's line/ring +detection. + +Kept negatives: on a NOISELESS signal the peel runs to completion and can use a couple of extra "cleanup" +levels -- the harmonic fits leave small Gibbs residue that is itself fit-able -- so 4 levels on a noiseless +trend+sine where ~2 are conceptual; a 1%-of-input negligible-residual guard plus the MDL gate bound it, and +on any NOISY (i.e. real) signal it halts at the noise floor (3 levels, not endless). trend+two-sines finds +nothing because the FIRST topology detection fails on the mixed signal -- peeling can only go as deep as +decompose_signal can see at each step. + +As above, so below: each peel level is a real decompose_signal call (the recorded topology and n_terms of +level 1 match a direct decompose_signal on the denoised signal), the ladder of laws is saved through the same +path as a single law (save was unified to handle one law or a ladder), and the whole thing is the same VSA +program with one APPLY swapped for an ITERATE. Real basis: matching-pursuit / iterative residual +decomposition (peel the dominant component, recurse on the residual) under the engine's MDL-gated +decompose_signal. + +Tests: +4 (831 -> 835), in test_procedure_faculty.py. + +## Deep synthesis, meet-in-the-middle, and the bind/permute collapse (SYN-1: a measured negative + its flip) + +SYN-1 asked: extend program synthesis past the depth-2/3 the forward BFS reaches, via a meet-in-the-middle +bidirectional search (forward from the input, backward from the output with inverse ops, meet in the middle -- +halving the search exponent). Before building it, the precondition was measured -- and the measurement killed +the plan, in the engine's usual humbling way. + +The cleanly-invertible ops are BIND (inverse: unbind) and PERMUTE (inverse: shift back). But that algebra +COLLAPSES. Measured at cosine 1.0000 on every case: two binds are one bind by the product, bind(bind(x,a),b) +== bind(x, a*b); a permute slides through a bind onto x, permute(bind(x,a)) == bind(permute(x), a); so ANY +interleaving of k binds and m permutes applied to x equals permute(x, m) bound by the product of all the +operands -- a depth-(k+m) program is a depth-<=2 canonical one. There is nothing DEEP to find in the +invertible algebra, so a bidirectional search through it buys nothing the M5 fingerprint (one example recovers +the kernel for exactly this linear/convolution class) does not already give. + +And the ops where depth genuinely matters -- BUNDLE (its superposition normalizes, which breaks +bind-commutativity, so bind/bundle programs do NOT collapse) and the nonlinear APPLY/ITERATE/IFMATCH/CALL -- +are precisely the ops that do not invert cleanly (bundle's inverse is "subtract and de-normalize," lossy with +an unknown scale; APPLY/cleanup are many-to-one). So the meet-in-the-middle backward search has no clean +target on the only programs whose depth is real. The forward-only BFS (M6) at depth 2-3 is the right tool; +its limit is the branching factor, not an algorithm a bidirectional trick could fix. Meet-in-the-middle is +NOT built -- it would be dead complexity. That is the negative, kept. + +The flip side is constructive. The collapse is exactly a program OPTIMIZER: `canonicalize_procedure` reduces +any bind/permute program to its minimal form -- the k binds become one bind by the product, the m permutes +stay m unit shifts (this VM's PERMUTE is a fixed shift of 1) -- and verifies the reduction by execution +(cosine 1.0). Measured: a 5-op program (bind;permute;bind;permute;bind) reduces to 3 ops (2 permutes + 1 +product bind); five binds reduce to one. It is also an EQUIVALENCE oracle for the invertible algebra: two +differently-written programs that compute the same function reduce to the SAME canonical form (bind a; +permute; bind b and permute; bind b; bind a both canonicalize to permute; bind(a*b)). BUNDLE and the +nonlinear ops are honest BARRIERS -- a program containing one is refused (fully_collapsible=False), not given +a wrong partial answer. + +As above, so below: canonicalize verifies through the same run_procedure path as everything else (it executes +the original and the canonical program and compares), and stores the product operand as a real codebook atom +so the canonical program is runnable. Real basis: the convolution algebra (binding is circular convolution -- +commutative and associative; a cyclic shift is convolution with a shifted unit impulse) -- the same +FFT-on-a-torus operator the whole engine rests on, here used to prove its own programs flatten. + +Tests: +4 (835 -> 839), in test_procedure_faculty.py. + +## Sublinear procedure recall: the forest index is premature; vectorize the scan instead (REC-1) + +REC-1 asked: index the procedure fingerprints in a HoloForest so recall is sub-linear instead of an O(N) +scan. Measured first, in the engine's usual way -- and the measurement said no, then said what to do instead. + +A HoloForest over N fingerprints was built and timed against the linear scan, with realistic queries (the M5 +implied kernel matches its fingerprint at cosine ~0.95-1.0, so the nearest neighbour is well-separated). Two +findings: (1) the forest is SLOWER than a linear scan for every realistic library size -- 3-8x slower at +N=50-1000, crossing over only around N~4000 procedures -- and that regime is UNREACHABLE, since the single +`define` library vector holds at most a few hundred functions before bundle crosstalk corrupts decode, so a +4000-procedure library cannot exist in this architecture; the sub-linear index is premature twice over. (2) +Accuracy is fine at realistic noise (forest top-1 == linear top-1 == 1.0 when the query is close to its +target), so the rejection is about speed, not correctness. + +The measurement pointed at the real fix. The existing fingerprint recall was an O(N) scan implemented as a +PYTHON LOOP -- one cosine call per candidate. The bottleneck was the loop, not the algorithm. Replacing it +with a VECTORIZED scan -- cache the fingerprints as one unit-normalized matrix at index time, then compute +every cosine in a single matrix-vector product (mat @ qhat) and argmax -- is 6-26x faster than the loop AND +3-7x faster than the forest, at every realistic N. Recall stays O(N) but runs at BLAS speed; the named-subset +path keeps the dict loop (rare). The right answer to "an O(N) scan is slow" was to vectorize it, not to reach +for a sub-linear index that does not pay until a scale the system cannot reach. + +As above, so below: the vectorized path returns the SAME identity and the SAME cosine the per-candidate loop +would (rows are unit-normalized so mat @ qhat is exactly cosine), verified against the loop on a real library. +Real basis: a linear nearest-neighbour scan as a single GEMV -- the standard "the constant factor, not the +big-O, was the problem" lesson, measured rather than assumed. + +Tests: +1 (839 -> 840), in test_procedure_faculty.py. + +## Operand prediction in recipe completion (GEN-1, shipped) + +complete_procedure predicts the next OPCODE of a partial recipe from a learned grammar (M4); GEN-1 adds the +OPERAND -- the full next instruction. learn_recipe_grammar now trains a second PredictiveMemory over the +JOINT (opcode, operand) token stream ("OPCODE|operand"), and complete_instruction predicts the next joint +token and splits it back into (opcode, operand, confidence). The opcode grammar is untouched, so +complete_procedure is unchanged. + +Measured, with the honest boundary front and centre. When operand USAGE is PATTERNED -- two templates, +a->b->c and d->e->f, the operand determined by context -- operand prediction generalizes to held-out recipes +at accuracy 1.00 (it learns the context->operand map, returning ("BIND","b") after "BIND a" and ("BIND","e") +after "BIND d"). When operands are ARBITRARY per recipe, the operand is unknowable: held-out operand- +prediction accuracy falls to chance (0.23, vs 1/6 = 0.17 for six operands) -- correctly, a random operand +cannot be anticipated. The opcode SHAPE, meanwhile, is predicted operand-independently in BOTH cases (the +opcode grammar ignores operands), so complete_procedure stays the robust call and the operand is a bonus only +where it is patterned. + +Kept negative (and a sharp one): the CONFIDENCE is not a reliable abstention signal for operands. An n-gram +context seen only ONCE returns confidence 1.00 -- a single random observation looks as certain as a +thousand-fold pattern -- so a high score does not mean the operand is really predictable. The honest +discriminator is held-out GENERALIZATION, not the confidence the model reports; that is why GEN-1 is +documented by an accuracy measurement, not a confidence threshold. + +As above, so below: complete_instruction delegates to the same PredictiveMemory class the opcode grammar uses +(a parallel model over joint tokens, seeded distinctly so the two never mix), and with no grammar it returns +(None, None, 0.0) -- backward-safe. Real basis: an n-gram / Markov sequence model over instruction tokens +(the recipe grammar), here over the joint opcode-operand alphabet rather than opcodes alone. + +Tests: +3 (840 -> 843), in test_procedure_faculty.py. + +## Above/below sweep: the cleanup-matvec pattern, and a denoiser promoted out of the pipeline (shipped) + +An "as above, so below" sweep -- looking for a technique or primitive that is load-bearing in one place and +applies elsewhere. Two findings, both acted on. + +(1) The VECTORIZED-RECALL pattern. The core Vocabulary.cleanup already snaps a query to the nearest atom with +ONE matrix-vector product against a cached stack of the stored vectors, not a Python loop of per-name cosines +("a constant-factor win, not a big-O one" -- the argmax is identical because stored atoms are unit length, so +the dot IS the cosine up to the query's norm). The sweep found four more recall paths still written as the +loop, and gave each the same cached-matrix treatment, every one bit-for-bit identical to the loop it replaced: + + * ScalarEncoder.decode -- was re-encoding a 200-point grid AND cosine-scanning it on EVERY call; now the grid + encodings are built once and cached as a unit-normalized matrix, decode is one matvec. Measured ~224x. + * archive recall + recall_by_tags -- were looping over stored fingerprints / tag-addresses; now cached + matrices (invalidated on add()), one matvec each, untagged images masked to score -1 as before. ~4-16x. + * Lexicon.nearest -- was a per-word cosine over the whole vocabulary; now a matvec against a cached + (row-normalized) meaning matrix + a top-k, same ranking. Measured ~20-40x on a 500-10000-word vocabulary. + The cache remembers which meaning dict it was built from, so a re-bootstrap rebuilds it automatically. + * market nearest_motif -- the per-window cosine loop became one matvec (no cache: past windows are passed in). + + Deliberately LEFT (the principle is "earn it by measurement," not vectorize everything): the region-router's + scan is over num_partitions (small N, no win); a benchmark accuracy helper is not a faculty path; the + resonator and reasoning factor-recall already use per-factor matrices. Recorded so the non-action is a choice. + +(2) A PRIMITIVE PROMOTED below. The data-analysis pipeline owned a private _denoise_signal: the only way to +clean a LONE 1-D signal (which the denoise faculty otherwise can't do -- a single vector has no manifold, and +nlm needs a patch SET, not a raw signal). It builds the prior from the signal ITSELF -- a smooth signal's +sliding-window Hankel matrix is low-rank (Broomhead-King / Cadzow SSA), so the windows project onto their own +subspace and the signal rebuilds by anti-diagonal averaging. That is a general capability, not a pipeline +detail, so it moved DOWN into holographic_denoise as trajectory_denoise and is exposed as +denoise(method='trajectory') -- the second prior-free denoiser beside nlm. The pipeline's _denoise_signal is +now a one-line delegate to it (bit-for-bit identical, max abs diff 0.0), so the pipeline and any other caller +share one implementation. As above, so below: the faculty method delegates to the module function, and the +pipeline (above) delegates to the faculty method. + +Real basis: matrix-vector recall is just the inner-product nearest-neighbour every cleanup already is; SSA / +Cadzow trajectory denoising is the lone-signal classic. Honest negative carried in the new method's docstring: +trajectory denoise has nothing to recover from a STRUCTURELESS signal (its trajectory is full-rank) -- the +prior is the signal's own structure, so a signal without structure can only be shrunk, not restored. + +Tests: +5 (843 -> 848): vectorized-recall == loop pinned for scalar decode, archive recall, and lexicon +nearest; the trajectory method shown to clean a lone signal and to be the exact denoiser the pipeline delegates +to. The four faculties' own existing suites still pass unchanged (the behavior is identical, only faster). + +### Above/below sweep, second pass (the honest non-finding) + +A follow-up pass looked for MORE of the same -- duplicated primitives, faculties re-implementing kernel +machinery, hand-set thresholds that should be data-derived. The honest result is mostly a CONFIRMATION that +the engine is already well-factored, which is worth recording so the absence of changes is a measured choice, +not a missed opportunity: + + * `bind` is centralized -- zero faculties re-implement the FFT circular-convolution by hand; everyone + delegates to the one kernel operator (the discipline the trajectory-denoise promotion just reinforced, + already holding for the core algebra). + * The procedure-matched nulls (`_recognition_null`, `_scan_cue_null`, `_brain_null`) are DELIBERATELY + separate -- each must match its own recall procedure, so a shared generic null would be anti-conservative. + Consolidating them would VIOLATE the project's own "procedure-matched nulls" rule; leaving them apart is + correct. + * `box_resize` is defined once and shared by the archive and the splat-archive; `learn_dynamics` works on a + given state SEQUENCE, not a delay-embedded 1-D signal, so it shares no Hankel construction with the new + trajectory denoiser -- no duplication to fold. + * The handful of inline cosines that aren't the shared helper are mostly DIFFERENT operations -- a COMPLEX + inner product (mobius) and a mean-centred correlation (a couple of measurement spots) -- which the plain + real `cosine` helper would silently get wrong, so they correctly stay bespoke. + +The one genuine residue: the Flask app (`app.py`) carried a private `box(img, n)` that duplicated the colour +branch of the shared `box_resize` (and even hard-coded 3 channels, so it would crash on a grayscale image the +shared function handles). It now imports and calls `box_resize` -- one duplicate removed, bit-identical on the +3-channel images it is used on. Cosmetic and app-layer (no test-count change), but a real DRY fix. + +Net: the high-value above/below seam was the vectorized-recall pattern and the lone-signal denoiser (first +pass); the second pass confirms the rest of the codebase already honours the discipline, with one app-layer +duplicate folded away. Writing the non-finding down is the same honesty the rest of the project runs on. + + +## TopK resonator readout: the high-load option (shipped) + +The SBC resonator's alternating-projection blend had two readouts -- softmax (the original, blends all atoms) +and sparsemax (Martins & Astudillo 2016, blends only the relevant ones, curing softmax's metastable mixing +and raising capacity at the MIDDLE of the load range). A third, TopK (Gao et al. 2024 -- the k-sparse +autoencoder readout; a point in the same Hopfield-Fenchel-Young energy family), keeps exactly the k largest +atoms and softmaxes over just those. MEASURED on the capacity cliff (all-factors-correct, F=3, B=16, L=64, +vs codebook size N): TopK wins at the HIGHEST load -- at N=110, where softmax, sparsemax, AND alpha-entmax all +collapse to 0.05, topk(k=8) is the only readout still recovering factors (0.23), and it leads at N=50 (0.60 +vs sparsemax 0.47). A fixed k keeps k candidates alive where adaptive methods over-prune. + +Kept negatives: k must be chosen (k=4 underperformed k=8 badly), and TopK ties or slightly LOSES to sparsemax +in the MIDDLE of the range (N=80: 0.12 vs 0.25) -- so it is the high-load option, not a new default. +alpha-entmax (the principled softmax<->sparsemax interpolation at alpha=1.5) was ALSO prototyped and DECLINED: +it merely tracks sparsemax, finding no sweet spot the annealed resonator benefits from -- the extreme (TopK) +wins, not the interpolation. The Hopfield-Fenchel-Young framework (Santos et al. 2025) is the theory that +unifies softmax/sparsemax/entmax/TopK as one energy family; the measurement still rules. + +Above/below: TopK is a readout PRIMITIVE, so it lives in holographic_hopfield beside _sparsemax (the shared +cleanup home, no reimpl) and is imported by the resonator -- one readout family, used wherever a softmax over +similarities is. Threaded through decompose_structure, resonator_confidence, and the mind's +decompose_structure faculty with a `k` parameter; the calibrated-confidence null caches per (readout, k) so +its p-value stays matched to the chosen readout. Backward-compatible: default stays softmax. + +Tests: +1 (848 -> 849): test_topk_readout_recovers_and_verifies in test_holographic_sbc.py. + + +## Support-weighted soft predict: MAP-correct on stochastic successors (shipped) + +Re-reading Closure-SDK (faltz009) through the lens of "the substrate stores but does not compute" confirmed +the engine ALREADY owns the predictive loop it inspired (PredictiveMemory: build_predictor / anticipate / +generate_predictive, plus the meaning-level MeaningPredictor). A re-derivation prototype only reconfirmed +that holostuff does context-sensitive prediction (A->B after P, A->D after Q: 1.00 vs 0.55 for Markov), +generalizes to held-out sequences of the same grammar (1.00), and rolls out STABLY over 500 steps with no +drift -- the discrete cleanup resets the representation each step, so the limit is context ORDER (the n-gram +curse), not drift, and under-ordered it degrades to wrong-but-VALID, never garbage. No new faculty earned a +place. + +But the Closure-style probabilistic test (a context with two successors at 70/30) surfaced a REAL BUG in the +existing faculty: the SOFT read (zread blend of next-vectors) weighted candidates by resonance only, so two +equally-resonant entries (same context, cosine 1.0) contributed EQUALLY regardless of how often each was +seen -- a 70/30 split blended 50/50 and decoded to the 30% (minority) symbol. The fix is the engine's own +frequency-weighted-superposition insight (the same trick that lets the scene coder count and recover objects +from an unnormalised sum): weight the soft blend by each entry's SUPPORT (its reinforcement count, already +tracked). MEASURED MAP-correct across mixes 60/40..90/10. The same revisit showed the HARD read is itself +fragile near a tie (at 60/40 it returned the minority), so support-weighted soft is now the reliable read on +stochastic streams. zread gained an optional `weights` parameter (default None = unchanged); it has exactly +one caller, so the change is fully backward-compatible. + +The honest outcome of the Closure re-read: not a new capability (the predictive loop, itself ported from +Closure, was already shipped) but a precise fix to it -- plus verification that the engine's scattered +prediction faculties ARE a from-scratch attention/predictor that learns WITHOUT autodiff, the project's hard +constraint, vindicated by an independent geometric computer reaching the same place. + +Tests: +2 (849 -> 851): test_zread_support_weighting_picks_the_frequent_value and +test_soft_predict_is_map_correct_on_stochastic_successor in test_holographic_predictive.py. + + +## Soft (sharpened) cleanup for the older resonators (above/below sweep, shipped) + +The SBC resonator's readout lesson -- a softmax-SHARPENED blend beats the raw-similarity superposition -- +swept downward to the two older resonators, which both used hard/linear cleanups. MEASURED, both win at +high codebook load, with the size of the win set by how strong each resonator already was: + + * BIPOLAR resonator (holographic_resonator.py, elementwise-product binding, sign cleanup): replacing + sign(B.T @ (B @ est)) with sign(B.T @ softmax(beta * cosines)) gives a 3-25x recovery improvement + (F=3, D=1024, 5 restarts): N=50 hard 0.30 -> soft 0.93; N=70 0.03 -> 0.77; N=90 0.00 -> 0.53. And it + needs far fewer restarts -- one soft run (0.47) beats five hard restarts (0.30) at N=50. + * CIRCULAR-CONVOLUTION resonator (holographic_reasoning.py, the engine's native bind/unbind, continuous + cleanup): smaller win because it is already much stronger (linear handles N<=30 perfectly). Through the + real class, N=35 0.90 -> 1.00, N=45 0.90 -> 0.95 (beta=25). The scene coder, which uses it, is already + near-ceiling on its tiny codebooks (colours 7, shapes 4, textures 4): linear recovers K=7 objects at + 1.00 and K=9 at 0.96; soft reaches 1.00 at K=9 (+0.04, marginal). + +THE BETA SWEET SPOT is unimodal and load-bearing (kept negative): too soft (beta<=5 on cosines) is too flat +to converge and fails completely; too sharp (beta>=80, or beta applied to RAW unnormalised similarities, +which is effectively one-hot) collapses the search and also fails. The win lives in the middle -- beta~30 +bipolar, ~15-30 circular-conv -- a sharpening that keeps a thin tail of competitors alive so the resonator +can still explore. (The first prototype FAILED because beta=8 on raw sims of scale ~1024 was effectively +one-hot; normalising to cosines and using a fair beta is what surfaced the win -- recorded so the scaling +trap is on the record.) + +SCOPING HONESTY: the biggest win (bipolar, 3-25x) lands on factor_composite's LEGACY dense path; the +actively-used circular-conv callers (scene coder, the CRT ResidueSystem in holographic_extras) run small +codebooks where the current cleanup is already near-ceiling, so the practical gain there is marginal. The +lesson genuinely transfers, but its high-impact regime (large codebooks / high load) is one the current +callers rarely hit. Wired therefore as a backward-compatible OPTION (a `beta` parameter on +ResonatorNetwork.factor, default None = the original linear cleanup, no default changed) so any high-load +use -- larger CRT moduli, the legacy path, a future large-codebook factorization -- gets the win for free, +without churning the paths that don't need it. + +TWO CLEAN NON-FINDINGS from the same sweep, written down because the absence of a change is a measured +choice: (1) the MoE gate already does sparse top-1 routing, and its docstring records that BLENDING experts +was measured to HURT -- so top-k (k>1) routing would re-introduce a known failure; the lesson does not +transfer. (2) the MeaningPredictor already embodies the frequency lesson by construction: fit_transitions +stores one entry PER OCCURRENCE (no merge), so its coupling-weighted blend is frequency-weighted by +repetition -- verified MAP-correct (after [p,a] with b 70%/c 30% it returns 'b'), and its settle already +uses a top-5 readout. The zread support-weighting fix it would have needed is already there, reached a +different way. + +Tests: +1 (851 -> 852): test_resonator_soft_cleanup_beta_recovers_where_linear_fails in test_holographic.py. + + +## Magic-number sweep: the beta=25 cleanup sharpness, checked and justified (sweep, no code change) + +A sweep for magic numbers -- arbitrary constants that gate behaviour and might be derived or calibrated +instead of hand-set. FINDING FIRST: the codebase already runs a strong no-magic-number discipline -- +statistical z-floors that say "exceeds noise" rather than a tuned cutoff, natural-largest-gap splits, the +auto coherence floor derived from the store's own distribution, the calibrated decide_confidence that +replaced a hand-set blind_floor. Most thresholds are already data-derived statements, not magic. + +The one prominent remaining magic number is beta=25.0 -- the softmax-cleanup SHARPNESS, the default across +Vocabulary.cleanup, dense_cleanup, codebook_denoise, and HopfieldCleanup. Three derivation hypotheses were +prototyped and MEASURED; none cleanly beats a sensible fixed value, so the constant earns its place: + + 1. SHOULD beta scale with dimension? The principled window is ln(N) < beta < sqrt(2D) -- the softmax must + beat the noise pack (N-1 random cosines ~ N(0, 1/sqrt(D))) but not amplify a noise fluctuation into a + false winner. MEASURED the resonator's sweet-spot beta at D=512..4096: it sits in 15-40 and does NOT + cleanly track sqrt(D) -- lower beta is favoured on HARDER problems (low D), and beta barely matters when + D is large (codewords already well separated). No clean formula. beta=25 is within the acceptable range + at every D (optimal at 2048); a slightly lower 15-20 is marginally more robust across D. + 2. DOES sparsemax remove the beta dependence? For PLAIN (non-iterative) cleanup, YES and cleanly: on a + continuous manifold, softmax recovery climbs with beta (needs it high enough to stop over-smoothing; + beta=25 good-not-maximal) while SPARSEMAX recovers ~1.000 across the ENTIRE beta=4..150 range, D- and + density-independent -- the simplex projection self-adapts its sparsity, so beta stops mattering. The + magic number's RISK is thus already neutralised where it matters most: the codebase ships sparsemax as a + measured-better option for the continuous case. KEPT NEGATIVE: in the ITERATIVE resonator sparsemax is + NOT beta-robust -- it just shifts the sweet spot LOW (beta=8: 1.00 vs softmax 0.53; beta=60: collapses + to 0.60), because its projection reaches one-hot at lower beta and one-hot breaks the resonator search. + 3. DOES annealing beta (soft->sharp, the SBC resonator's own design) remove the need for a fixed value? For + the DENSE circular-convolution resonator, NO -- annealing from a very soft beta0=0.5 HURTS at every D + (0.53-0.80 vs the fixed value's 0.67-1.00): the early super-soft iterations blend everything and waste + the descent. Annealing works for the SBC (sparse block code) resonator but does not transfer to the dense one. + +Bottom line: beta=25 is a JUSTIFIED default, not loose magic -- safe and even conservative for plain cleanup +(where sparsemax already neutralises any beta-sensitivity), and a defensible fixed compromise for the +resonator (where beta is an irreducible tuning knob that does not derive cleanly from the geometry). The +actionable nuances: prefer sparsemax when cleanup beta-sensitivity is a concern; the resonator's beta sweet +spot is readout-dependent (softmax ~25, sparsemax ~8) and mildly load-dependent (lower at low D). No code +change and no test-count change -- writing the measured non-finding down so the three derivation attempts are +not re-run. + +## De-Doppler drift detection: a binding-is-a-shift, in radio astronomy (DDD, shipped) + +The detection cluster (Tarter, Siemion, Cranmer seats) had every primitive a turboSETI triage needs -- +streaming SPRT, FDR, calibrated nulls -- but no faculty that put them on the field's actual signal: a +narrowband technosignature that DRIFTS in frequency (the Doppler shift from relative motion). The whole +detector turned out to be two engine primitives reused, no new machinery: + +- A Doppler drift is a cyclic SHIFT of the spectrum over time, and a cyclic shift is the engine's `permute` + (np.roll) -- the SAME rigid-shift transform holographic_video.py uses for motion-compensated compression, + and equally a binding: bind(x, delta_k) == permute(x, k), exact to 1e-9. So "de-Doppler integration" -- the + matched filter that recovers a drifting signal a stationary detector loses -- is just permute-ing each frame + back by the drift before summing. The bank sweeps every candidate drift; the peak reports BOTH the signal and + its drift rate. +- The look-elsewhere control over the (drift x channel) grid is `bh_fdr` (dependent / Benjamini-Yekutieli -- + the drift cells overlap, so the tests ARE dependent), exactly as `scan` controls it across channels. + +MEASURED on synthetic spectrograms at the field's S/N>=10 regime: +- Stationary integration LOSES a drifting signal (~2.9 sigma, noise level); de-drift at the right rate RECOVERS + it (~6.2 sigma). The de-drift via the bind kernel is bit-identical to np.roll (the shift-is-a-bind identity). +- Look-elsewhere: naive per-cell thresholding fires on ~100% of pure-noise scans (1664 cells); bh_fdr on ~0%. +- ROC: recall ~96% at 0% false-positive at integrated ~12 sigma (the field's <1%-FP-@-95% bar). +- ON-OFF cadence: a STRONG stationary RFI (S/N 12) that WOULD be detected on its own is rejected ~100% (it + persists in the OFF pointing), while the drifting ON-only signal is kept ~94%. + +KEPT NEGATIVE: below ~10 sigma integrated, recall falls off -- the dependent-FDR correction over the many cells +is conservative (a lone weak signal needs ~5 sigma to clear the multiple-testing bar). Not a flaw but a match: +turboSETI's own search threshold is S/N>=10 precisely because it scans so many places. The detector is as honest +about the cost of the bank as the field is. Structurally this is also the learn_dynamics move (a known operator +advancing/reversing a state): de-Doppler integration == recall_at over the drift operator. + +Wired as `UnifiedMind.detect_drifting(waterfall, drifts=None, alpha=0.01, off=None)` beside the scan/stream +detection faculties, delegating to holographic_dedoppler (dedoppler_bank + detect_drifting). Deterministic. +Tests: +3 (852 -> 855), in test_holographic_dedoppler.py. + +## Above/below sweep after de-Doppler: pain-point hunt, one optimization shipped (sweep + DDD-opt) + +The discipline (Moose): after unblocking something, old negatives may be retired and new paths open; and sweep +for places we are SLOW, skip compression, or do work we don't need. Findings, all measured: + +- **de-Doppler bank vectorization -- KEPT NEGATIVE.** Replacing the bank's per-(drift,frame) `permute` loop with + a fancy-index gather is FASTER for small inputs (6.7x at T=24,F=96) but SLOWER at scale (0.5x -- a regression -- + at T=128,F=2048): np.roll's C implementation beats a fancy-index gather once arrays are large. The shipped loop + is the right choice. The genuine fast algorithm is the Taylor-tree / fast-folding de-Doppler (turboSETI's, + O(F log T)), a real algorithm not a quick vectorize -- noted as the scale path, not built. +- **Codebase already well-vectorized.** A grep for per-item cosine/dot loops in hot paths found only ONE + (`holographic_archive.py` `tags_of`, on small tag lists -- not a pain point). The GEN-1 vectorized-recall sweep + did its job. +- **SHIPPED FIX -- resonator confidence null keyed by SHAPE, not content.** A cProfile of a confidence workload + showed `_resonator_noise_null` dominating (12 of 13 s, 833k FFTs). The cache key hashed full codebook CONTENT, + forcing a multi-second cold fit for every new codebook set. Measured: across five random codebook contents of + one shape the p-value is IDENTICAL for every decision-relevant agreement (>=0.45 -- the regime where a + factorization is trustworthy); content only shifts the deep-abstain tail (agreement ~0.27, answer "abstain" + regardless). So the content hash was redundant work. Dropped it; the null is now keyed on shape + (B, L, codebook sizes, restarts, iters, readout, k). Result: three DIFFERENT codebook sets of one shape now + cost ONE cold fit (7828 ms) + two 13 ms cache hits, not three cold fits; one fit per shape for a whole run / + test suite. Calibration verified UNCHANGED (the corruption sweep p-values match the pre-change A2 result + exactly: p 0.010 while factors recover, rising to 0.57 as they fail). The resonator's FFT inner loop has a + further ~30-50% available by caching per-factor rffts across the leave-one-out binds, but that touches the + tie-sensitive per-block kernel (the bind_batch lesson) so it was NOT taken -- the cache-key fix is numerically + identical and safe. + +Tests: +1 (855 -> 856), in test_integration.py (test_resonator_null_keyed_by_shape_not_content). + +## Self-verifying storage: the holographic Merkle tree (BLD-1, shipped) + +The one genuinely new capability the cross-project comparison surfaced: tamper-evidence as an O(log n) property +of the structure itself, with no prior engine analog -- and it rides entirely on the two kernel primitives, so +it stays in-substrate. A segment tree whose COMBINE is bundle over POSITION-bound items: leaf_i = bind(pos_i, +item_i); each internal node = sum of its children; root = the whole-store composite. DETECT by rebuilding the +root from current items and comparing; LOCALISE by descending from the root, following the child whose composite +no longer matches its committed value -- the changed item in <= log2(n)+1 comparisons (root check + descent +depth), independent of n. + +Measured (the BLD-1 bar, deterministic): a single full tamper localised 40/40 in <= log2(64)+1 = 7 checks, 0 +false positives on clean data; a slot SWAP detected and localised (position binding defeats the bundle's +commutativity -- without it a reorder is invisible). + +KEPT NEGATIVES (measured, load-bearing): +1. LINEAR, NOT CRYPTOGRAPHIC -- the headline. The root is a linear combination, so items -> root is + R^(n*D) -> R^D, many-to-one for n>1: collisions EXIST and are CONSTRUCTIBLE. A key-aware adversary picks any + change da to item a, then changes item b by db = deconv(-bind(pos_a, da), pos_b) so bind(pos_b, db) exactly + cancels it -- the root is bit-for-bit unchanged (measured: cosine 1.0000, an invisible forgery). A + cryptographic Merkle tree resists this (a hash collision is hard; cancelling a linear sum is a division). So + the guarantee is evidence of ACCIDENTAL corruption / uncoordinated tampering, NOT tamper-proofing. +2. SINGLE-TAMPER LOCALISATION -- the descent follows one differing child, so several uncoordinated changes are + detected at the root but only one path is returned per pass. +3. O(n) SPACE -- localisation needs the per-node composites kept; a root-only commitment is O(1) but detect-only. + +A guess the plan got WRONG, kept on the record: quantising the stored checksums to save space was expected to +create a small-tamper detection floor (the "keep leaves in capacity" worry that bounds superposition elsewhere). +Measured, it does NOT bind here -- detection stays 100% down to 2-bit checksums even at n=1024, because in high +dimension a tamper always pushes some component across a quantiser boundary. So the checksums are kept exact +float and the quant option is not exposed. + +Tests: +5 (856 -> 861), in test_holographic_verify.py (selftest wrapper + single-tamper localise-in-log-checks + +position-binding-catches-reorder + the linear-collision kept negative + the verify_store faculty round-trip). + +## External-baseline benchmark harness (BLD-2, shipped) + +The field's most convincing habit -- "N times the standard tool for a real job" -- adopted with the project's +discipline: the case where the standard tool WINS stays on the record. INV-2 first audited which baselines are +even reachable in NumPy/Flask-only (sklearn/faiss/statsmodels are banned): general-purpose compression has a +fair stdlib opponent (zlib/lzma), exact NN is a fair opponent for sublinear recall; denoising/forecasting/ +classification have no fair in-constraints standard tool, so they are NOT benchmarked rather than measured +against a strawman. + +Two runnable, deterministic comparisons in benchmarks/ (numbers checked into benchmarks/README.md): + +bench_compression.py -- the geometry-preserving rd code (quant='rd': KLT + water-filling + rANS) vs int8 fed to +zlib/lzma, at matched cosine fidelity. MEASURED (bits/vector): on rank-8 structured data rd wins ~34x at N=2000 +(111 vs ~3800; the KLT basis amortizes over the batch, so the win grows with N); on FULL-RANK RANDOM data rd +LOSES (6851 vs 3826 at N=2000) -- no low-rank to exploit, the basis costs more than it saves. The kept negative: +rd is the right tool exactly when the data is low-rank, which the engine's stored states are and random vectors +are not. + +bench_recall.py -- the HoloForest (sublinear approximate NN) vs an exact items@query scan. MEASURED: the forest +matches exact recall@1 up to ~10k items and touches a shrinking fraction of the comparisons (467/826/952/616 = +93%/41%/11%/3% as N goes 500->20000); recall@1 holds 100% to 8k, 97% at 20k. The kept negative: on WALL-TIME the +exact scan is one BLAS matvec, so fast that the forest's pure-Python traversal only overtakes it past ~20k items. +The forest buys sublinear WORK (what matters when comparisons are expensive or N is large), not raw wall-clock at +small N against a tight BLAS loop. + +Tests: +3 (861 -> 864), in test_benchmarks.py (rd-wins-on-structure-loses-on-random + reproducibility + +forest-sublinear-and-near-exact). + +## INV-5: unblockable-negative profiling pass (shipped) + +The discipline: unblocking something (denoising, learned energy, SBC, the rd-code, the vectorized-recall +pattern) can retire an old negative -- old results may no longer hold -- so re-profile the heavy non-confidence +workloads and re-scan for missed loops. cProfile + a STATIC scan for the loops INV-1's cosine-grep could not +see: loops that call a similarity HELPER rather than a literal cosine / @ / np.dot. + +Found ONE real win, on the record as a table: +- FHRR PhasorVocabulary.cleanup -- a per-candidate `for nm in names: fhrr_sim(noisy, self.vectors[nm])`. INV-1 + missed it precisely because fhrr_sim is a helper, not a literal cosine. VECTORIZED to two REAL matvecs + (real(vdot(b,q)) == Re(b).Re(q) + Im(b).Im(q)) with the stacked real/imag matrices cached -- ~11x at k=2000 + (19.6 -> 1.8 ms), argmax-identical, sims matching to ~1e-16. The honest subtlety: the OBVIOUS vectorization + `np.real(B.conj() @ q)` is NO faster than the loop, because B.conj() allocates a full complex copy whose cost + matches the matvec -- the real-matvec-no-copy form is the actual win. SHIPPED. +- archive recall_by_tags -- its for-loops are TAG set-logic (words/nums), not similarity-over-N; not a target. +- Lexicon.nearest -- already vectorized in the prior above/below sweep; the residual loops format the top-k. +- creature decide -- has per-action loops, but it is the TIE-SENSITIVE maze path the bind_batch lesson keeps + hands off (a 1e-12-identical change once flipped a trajectory). NOT touched, by policy. +- recurrent/_next_dist, schema/_ppm_dist -- small-alphabet n-gram/PPM distributions, not similarity-over-N. + +So the pass confirms the engine is otherwise well-vectorized (reinforcing INV-1 and OOS-1): one genuine missed +loop, fixed; everything else either already done, not a similarity loop, or deliberately tie-protected. + +Tests: +1 (864 -> 865), in test_holographic_fhrr.py (vectorized cleanup matches a brute-force loop on both the +all-atoms and the subset paths, and the cache invalidates when a new atom is minted). + +## BLD-3: theory-and-guarantees document (shipped) + +The project's notion of rigor, finally consolidated. THEORY.md gathers the load-bearing claims into one place +and tags each by what backs it: [CITED] for a literature theorem (Plate HRR capacity, Smolensky tensor binding, +Ramsauer/Krotov modern Hopfield, Wald SPRT, Benjamini-Hochberg/Yekutieli FDR, Tero flow, Duda ANS, Dasgupta +RP-trees), [MEASURED] for a result proven here with a named test pointer, [KEPT NEGATIVE] for a measured limit. +The rule: no claim appears without either a citation or a test. Sections -- the algebra (the +bind=convolution=phase=tensor identity), capacity (the cliff + the modern-Hopfield superset + FHRR), geometry & +compression (consolidation/KLT + rd vs zlib), search (RP-forest + Tero), detection & honesty (RecallNull + SPRT ++ FDR), self-verifying storage (the linear-collision negative), determinism (the bit-exact tie-break +discipline), and a table of the standing kept-negatives. + +A test (test_theory_references.py) parses THEORY.md and asserts every `test_file.py::test_function` it cites +resolves to a real function -- so the document is SELF-BACKING and cannot rot into asserting a test that no +longer exists. This is the doc's own guarantee, in the engine's measure-don't-assert spirit. + +Tests: +1 (865 -> 866), in test_theory_references.py. + +## BLD-4 & BLD-5 (de-Doppler scale/precision): prototyped/reasoned, NOT wired -- kept so they aren't re-tried + +Both were the conditional ("only if scale/precision matters") tail of the BLD list. Resolved honestly rather +than built speculatively: + +BLD-5 -- sub-bin de-drift via a fractional (Fourier phase-ramp) shift instead of the bank's integer-bin +`permute(wf[t], -int(round(d*t)))`. PROTOTYPED and MEASURED. First, a real bug to note: the 2-D `fourier_shift` +is NOT 1-D-safe -- on a 1-D frame its `reshape([F,1])` broadcasts `(F,)*(F,1)` into an `(F,F)` array (it +silently produced a spurious 50-sigma peak until a correct 1-D phase-ramp shift was used). With the correct 1-D +shift: a MODEST peak-SNR gain on sub-bin drifts, largest at half-integer rates where integer rounding is worst +(+0.7 sigma at drift 0.5; marginal at 0.3; tied on integer drifts). CRUCIALLY, NO drift-rate-resolution benefit: +on a fine (0.05) drift grid the INTEGER bank already recovers sub-bin drifts (0.25/0.5/0.75) with ZERO error -- +the matched-filter peak lands on the true drift regardless of the per-frame rounding. So the item fails its own +bar ("strictly better drift-rate recovery"): a fraction-of-a-sigma SNR gain, no resolution gain, an FFT-per-frame +cost. Does NOT earn its place; the integer de-drift stays. Kept negative. + +BLD-4 -- Taylor-tree / fast-folding de-Doppler (O(F log T) vs the bank's O(F*T*n_drift)). PARKED, on the same +discipline as OOS-1 (the native-kernel rejection): a SCALE-ONLY optimization with no demonstrated current +bottleneck (the bank runs fine at the verified spectrogram sizes), and the tree's bit-identity is non-trivial +for the FRACTIONAL drift grid the bank searches (the classic Taylor tree computes integer-slope sums). The right +trigger is a real SETI-scale search that profiling shows the loop cannot clear -- with numbers in hand, not +before. The Taylor-tree is the known approach when that day comes. + +## BLD-7: N-dimensional fractional power encoding + compute-on-functions (shipped) + +The backlog framed FPE as a thing to BUILD to beat the "locality-preserving RBF approximation." Measurement +flipped the premise: the 1-D ScalarEncoder is ALREADY a fractional power encoder. encode(x) = +irfft(exp(i*scale*x*phases)) is literally "raise the base to power x"; its kernel_at is the Bochner kernel +(sinc/rbf); and because the engine's bind is circular convolution -- spectrum multiply = phase add -- +bind(encode(x), encode(s)) == encode(x+s) to numerical exactness (cosine 1.00000). So 1-D FPE, with +shift-as-bind and a designed kernel, has been here all along; "beat the RBF encoder" is moot because it IS the +FPE/RBF encoder. (Pinned in test_holographic_fpe.py::test_scalar_encoder_is_already_fpe_shift_as_bind_and_kernel.) + +The genuine addition (holographic_fpe.py, faculty m.vector_function_encoder) is the step up from a scalar to a +VECTOR domain and to FUNCTIONS, which the engine did not have: + * N-D encoding -- a point in R^n is encoded by binding one per-axis 1-D FPE per coordinate. A shift along any + axis is still ONE binding (2-D shift-as-bind cosine 1.00000), and the kernel is the PRODUCT of the per-axis + kernels (measured 0.920 vs the 0.914 product at a 2-D offset) -- an n-D RBF for rbf axes. + * Compute on functions -- f: R^n -> R as a weighted superposition of encoded points, f = sum_i w_i encode(p_i); + querying reads sum_i w_i kernel(q, p_i) (a holographic KDE: high at placed points 0.81/0.69/0.78, low at an + empty spot 0.06); and the WHOLE function translates by ONE binding, bind(f, encode(delta)) = sum_i w_i + encode(p_i + delta) -- the rigid-shift-is-a-bind trick the motion compensator uses, lifted to a function. + +KEPT NEGATIVES (measured in the selftest): the standing capacity cliff applies -- a function is a bundle, so +query separation (placed vs empty) decays as atoms pile up (+0.39 at K=2 -> +0.01 at K=128); and where a scalar +suffices the n-D machinery buys nothing, since 1-D FPE IS the ScalarEncoder. Reuses the verified ScalarEncoder +per axis (DRY) and the engine's bind/cosine -- no new dependency, nothing learned. + +Tests: +9 (866 -> 875), 8 in test_holographic_fpe.py + 1 faculty test in test_integration.py. + +## EXP-5 + EXP-6: the spectral structure kernel + the Laplacian eigenbasis basis-selector (shipped) + +One operator, two readings. holographic_spectral.py builds the graph and Hodge Laplacians from a point cloud or +a simplicial complex and exposes their eigendecomposition (C2: eigenvector signs fixed -- largest-magnitude +component positive -- so the basis is reproducible, the bind_batch tie-break class of bug). + +EXP-5 (the operator + its sanity): the cycle-graph Laplacian eigenbasis IS the DFT (eigenvalues 4 sin^2(pi +k/n)); a ring signal reconstructs from it to 1e-15. And the Hodge Laplacian's HARMONIC dimension equals the +Betti numbers -- 4-cycle (1,1), filled triangle (1,0), two components (2,0) -- the spectral route to topology +EXP-7 will use. + +EXP-6 (the basis-selector, generalising decompose_signal's hand-picked list): the path-graph Laplacian +eigenbasis IS the DCT/elementary basis (line denoise 1.134 == DCT 1.134, identical to 1e-6), and the cycle's IS +the harmonic basis -- so the Laplacian eigenbasis SUBSUMES the line->elementary / ring->harmonic special cases. +And it extends to manifolds the topology detector can only call "line": on a sphere, the kNN-Laplacian low +eigenvectors recover a smooth degree-2 field (denoise 1.925 from a noisy 6.112) where the line/index-order basis +barely helps (5.235). The data-driven basis is measurably right where the hand-picked fallback is not. Faculty +m.spectral_basis(points, k, n_basis) -> a SpectralBasis with decompose/reconstruct/denoise. + +KEPT NEGATIVES (C1/C2, on the record in the module): dense eigh -> moderate N (a huge sparse graph would need +scipy, a banned dependency); and where the manifold genuinely IS a simple line or ring, the hand-picked basis is +cheaper and exact -- the data-driven basis earns its place only where the topology is unknown or off the +hand-coded list. Only NumPy; nothing learned. + +Tests: +8 (875 -> 883), 7 in test_holographic_spectral.py + 1 faculty test in test_integration.py. + +## EXP-7: principled topology by persistent homology (shipped) + +detect_topology names a 1-D signal's shape from a hand-coded menu (line/ring/mobius/torus via harmonic fits). +EXP-7 (holographic_topology.py, faculty manifold_topology) reads topology straight off a point cloud: build a +Vietoris-Rips complex at scale eps, count holes by dimension (Betti numbers B0/B1/B2 = components/loops/voids +via B_k = n_k - rank(d_k) - rank(d_{k+1})), and keep the signature that PERSISTS across a scale band auto-set +from the cloud's median NN distance. Reproduces detect_topology on its cases (contractible -> (1,0,0) "line", +loop -> (1,1,0) "ring") and extends to ones it can't name: torus (1,2,1) and sphere (1,0,1). B1 orders +line(0) 892), 8 in test_holographic_topology.py + 1 faculty test in test_integration.py. + +## EXP-8: the Helmholtz-Hodge decomposition of an edge flow (shipped) + +The same boundary operators that count holes (EXP-5/7) take an edge flow APART. Added to holographic_spectral.py +(hodge_decomposition, denoise_flow; faculties on UnifiedMind). Any flow on a graph splits into three L2-ORTHOGONAL +parts: flow = gradient + curl + harmonic. + * GRADIENT = d1^T phi -- curl-free transport from a vertex potential (the source-to-sink part). + * CURL = d2 psi -- divergence-free circulation around the filled triangles (local rotation). + * HARMONIC = remainder -- both div-free AND curl-free: the GLOBAL circulation wrapping the holes. Its dimension + is exactly B1, so the harmonic part IS the flow's topology (meets EXP-7 on one complex). +Computed by least-squares solves of the graph Laplacian (for phi) and the triangle Laplacian (for psi); the +harmonic part is what neither explains. + +MEASURED: the split sums to the flow at 1e-16 and the parts are orthogonal to 1e-15; the harmonic part of a +random flow is div-free and curl-free to 1e-15; denoising a transport flow (drop curl) returns 1.088 from a noisy +1.915 and well past naive edge-smoothing's 3.592 (which over-smooths). KEPT NEGATIVE: on a TREE (no cycles, no +triangles) curl and harmonic are exactly zero -- nothing to circulate -- so all flow is gradient; this falls +straight out of the topology (B1=0, no triangles). For the Tero flow solver and graph-signal denoising. + +Tests: +5 (892 -> 897), 4 in test_holographic_spectral.py + 1 faculty test in test_integration.py. + +## EXP-9: Clifford Cl(3,0) geometric algebra as a parallel binding mode (shipped) + +A second way to bind, alongside circular-convolution bind -- the geometric product of Cl(3,0) (8-dim +multivectors, holographic_clifford.py, faculty m.clifford()). Like tensor_bind, NOT a drop-in: a parallel mode +whose seat is GEOMETRIC structure, specifically 3D rotations. The product is built from the blade Cayley table +(blades as bitmasks over {e1,e2,e3}; product = XOR of masks + a reordering sign; e_i^2=+1). + +THE WIN IT IS BUILT FOR (measured): a rotor R = cos(t/2) - sin(t/2) B rotates a vector by the sandwich v' = +R v R~. Composing rotations is EXACT -- the geometric product of two rotors IS the rotor of the composed +rotation (max err ~1e-15 over 200 random pairs vs applying them in sequence) -- and NON-COMMUTATIVE (the two +orders of a pair land ~0.66 apart on a probe vector). HRR's circular convolution is COMMUTATIVE, so it returns +one answer for both orders and carries that whole order-gap as unavoidable error; that gap, which convolution +provably cannot close, is the concrete sense in which Clifford beats it on rotations. Rotors are length- +preserving and exactly invertible by their reverse (~1e-15). + +KEPT NEGATIVES (measured / on the record): 2^d DIMENSION GROWTH -- Cl(n,0) needs 2^n components (Cl(3,0)=8, +Cl(10,0)=1024), affordable only for low-dim geometric domains, not a general high-D substrate (HRR's fixed-D +FFT bind is the right tool there). And it binds VERSORS, not arbitrary atoms -- a unit rotor times its reverse +is the identity (clean unbind), but a random multivector times its reverse is NOT, so this is a geometric- +transform algebra, not a general key->value memory like HRR. Narrow win-condition: a parallel tool for the +rotation-shaped corner, like tensor_bind is for the capacity corner. + +Tests: +9 (897 -> 906), 8 in test_holographic_clifford.py + 1 faculty test in test_integration.py. + +## BLD-8: optimal transport by Sinkhorn (shipped) + +A transport-geometry distance between distributions, for the case bin-wise metrics get wrong. Euclidean/cosine +compare two distributions height-by-height and are blind to WHERE the mass sits: two histograms with no overlap +are maximally far no matter how far apart they actually are (a peak at bin 12 reads as distant from bin 10 as a +peak at bin 40 does). The Wasserstein (earth-mover's) distance measures the least work to MOVE one onto the +other -- mass times the ground distance it travels -- so it keeps growing as distributions move apart even with +no shared support. holographic_transport.py (faculty m.wasserstein(a, b, cost, eps)) computes it by the Sinkhorn +algorithm: add an entropy term -> a Gibbs kernel K = exp(-C/eps) and a pair of alternating diagonal rescalings +(u <- a/(Kv), v <- b/(K^T u)) converging to the transport plan P; the distance is . + +MEASURED: matches the 1-D closed form W1 = sum |CDF_a - CDF_b| to ~1e-3 (W=10.000 on a shift-10 pair). The win: +a shift of 5/10/20 reads as a distance of 5/10/20, while Euclidean saturates flat at ~0.53 for every +non-overlapping shift and cosine collapses to ~0 -- both unable to tell a near miss from a far one. A custom +cost matrix changes the geometry (a ring/cyclic cost lets mass wrap around, shrinking the distance). + +KEPT NEGATIVES (measured): THE EPS KNOB -- too LARGE blurs the plan toward the independent coupling and inflates +the distance (a same-mean narrow-vs-wide pair with true W1~3.6 reads ~4.9 at eps=50); too SMALL underflows the +kernel between separated supports (exp(-C/eps) rounds to 0) into a broken answer (eps=0.01 on a shift-10 pair +returns 3.4 instead of 10). The default RULE scales eps to the cost (0.02 * median nonzero cost), sharp without +underflowing for well-conditioned cost matrices; a wide cost range wants an explicit eps or a log-domain solver +(out of scope). Also O(n*m) per iteration (dense kernel + matvecs). And the entropic self-distance W_eps(a,a) is +a small positive bias, not exactly 0 (debias via the Sinkhorn divergence if an exact zero is needed) -- here +self << cross is what matters. + +Tests: +9 (906 -> 915), 8 in test_holographic_transport.py + 1 faculty test in test_integration.py. + +## Above/below sweep: the Tero flow solver wired to the Hodge decomposition (shipped) + +A sweep of the three new geometry kernels (spectral / topology / transport) for where they belong lower or +apply elsewhere. The genuine, load-bearing finding: the Tero flow-conductance solver (holographic_flow.py) +computes a Poiseuille flux Q_uv = D_uv(p_u - p_v) on every edge each step, uses it to thicken tubes, and +DISCARDS it once it has the path. That flux is exactly the edge flow EXP-8's Helmholtz-Hodge decomposition +takes apart. + +WIRED: `tero_flux(nbr, start, goal)` exposes the converged signed flux (refactored to share a `_tero_converge` +helper with tero_solve, bit-identical -- flow tests still green). Faculty `flow_circulation(nbr, start, goal)` +splits it: GRADIENT = net source->goal transport (its divergence is the injected current, max 1.000 = I0), +HARMONIC = circulation around the graph's loops (dimension = B1, the loop count EXP-5/7 already measure). A maze +graph has no filled triangles -> no curl. Returns {loops (B1), redundancy (harmonic energy fraction), +transport_energy, circulation_energy, flux, edges, n_vertices}. + +MEASURED: on a 4x4 grid, B1=9 = E-V+1, gradient divergence exactly I0 at source/goal (0 elsewhere), harmonic +subspace dim == B1. The harmonic FRACTION is a previously-hidden read on the flow -- how much of the converged +flux circulates rather than transports: EXACTLY 0 on a tree (forced route), and on a loopy grid it varies with +mu (5x5 grid: 0.73 at mu=4 down to 0 at mu=1 -- at high mu, competing thick tubes leave more circulating flux). +This connects three kernels: the flow solver + EXP-8 (Hodge) + EXP-7/5 (B1 topology). + +A RESTRAINT kept on the record (also a sweep outcome): the flow solver builds its OWN conductance-weighted +Laplacian, deliberately NOT rerouted through the shared graph_laplacian -- the dynamics is tie-sensitive and a +different summation order could flip a trajectory (the bind_batch lesson). The shared HELPER was extracted +inside the module; the shared KERNEL was left alone. Not everything that looks duplicated should be merged. +Two other candidates were audited and rejected as forced: decompose_signal (1-D symbolic regression, not a +point-cloud basis projection) and Wasserstein-into-the-honesty-layer (the score distributions are already +characterized by their thresholds/likelihood ratios). + +Tests: +6 (915 -> 921), 5 in test_holographic_flow.py + 1 faculty test in test_integration.py. + +## Above/below sweep of the geometry toolkit: spectral denoise wired, transport/topology kept standalone (shipped) + +After the geometry toolkit (BLD-7, EXP-5/6/7/8/9, BLD-8) the standing above/below discipline was run over the +three new kernels (spectral / topology / transport), grounded in a live-code audit. The honest result: most +connections were ALREADY in place, and exactly one genuine gap was found and wired. + +ALREADY WIRED (confirmed, not re-done): the Tero flow solver -> Hodge split (transport vs circulation, +flow_circulation faculty) was already built; the spectral and GF(2) boundary-operator routes corroborate (the +selftest pins their Betti agreement) rather than duplicate; the flow solver's conductance-weighted Laplacian is +deliberately NOT routed through the shared graph_laplacian (tie-sensitive dynamics, the bind_batch lesson). + +THE ONE GENUINE WIRING -- denoise(method='spectral', points=...): the unified denoise faculty could map a LINEAR +subspace (manifold/adaptive, which need an example set) but had no map for a CURVED manifold's geometry, and none +of its methods could clean a lone scalar field on a point cloud. The EXP-5/6 graph-Laplacian eigenbasis is exactly +that map. New signature params: points=<(N,d) coordinates>, spectral_k=10, spectral_nbasis=12; x is the field over +those N points. MEASURED on a smooth field over a 2-sphere: cleans error 4.078 -> 0.862, where the geometry-blind +options barely move it (trajectory/SSA 3.113, fixed DCT low-pass 4.182) -- a linear/1-D prior cannot see a curved +manifold's smoothness. It is the only denoiser in the faculty needing no example set and no codebook, just the +cloud's own geometry (the nonlinear-manifold completion of Milanfar's "denoiser = manifold map" framing). + +KEPT STANDALONE (a finding, not a failure): Wasserstein found NO existing bin-wise distribution comparison to +improve -- the market compares price WINDOWS by cosine and forecasts by proper score, the de-Doppler search uses +the field-standard permute+matched-filter bank; persistent homology is blocked from the 1-D detect_topology by +its delay-embedding uneven-sampling negative. Forcing either would be churn. + +Tests: +1 (921 -> 922), test_spectral_denoise_faculty in test_integration.py. + +## Self-hosting: PnP restoration and B10 generation moved into VSA programs (shipped) + +Following the self-hosting audit (which mapped the boundary: a Python procedure is movable into a VSA program +iff it is ORCHESTRATION whose every step is a hypervector->hypervector map and whose state is one accumulator), +two of the engine's remaining canonical iterate-to-fixed-point loops were re-expressed as programs running on +the HoloMachine -- joining PIPE-1 (the data-analysis pipeline) and the matmul-iterate (a recurrent linear map) +that were already there. Neither replaces its fast Python faculty; each adds the BEING-DATA form (a stored, +composable, recipe-savable procedure -- process, not object). + +RESTORE -- Plug-and-Play/RED as a program: ITERATE [APPLY datafit; APPLY denoise]. `restore_procedure(y, +forward, adjoint, samples, mu)` fits a manifold prior from `samples`, configures the inverse problem +(`set_inverse_problem`), and runs the two-step body to a fixed point. The `datafit` handler is the gradient +step ACC <- ACC - mu*adjoint(forward(ACC)-y); the `denoise` handler is the prior. MEASURED: on a half-masked +low-rank signal (raw rel-error 0.863) it recovers to rel-error 0.167 -- the SAME error-to-truth as the Python +`pnp_restore` -- converging in 6 ITERATE iterations where the Python loop runs a fixed 40. + +GENERATE -- B10 diffusion as a program: ITERATE [APPLY diffuse] from a noise seed. `generate_procedure(codebook, +steps, seed)` configures a self-cooling `diffuse` handler (`set_generator`) that anneals beta up and injected +noise down per call; ITERATE halts when the sharpened cleanup reaches a fixed point on the manifold. MEASURED: +lands on the codebook manifold at cosine 1.000 (the same sample `hopfield.generate` produces), in 13 iterations +(the 12-step schedule + the convergence check). The generative PROCESS is now a stored, composable procedure. + +The schedule wrinkle is the one non-obvious bit: `generate` is a SCHEDULED loop (beta/noise change per step), +which a bare ITERATE (same body each step) cannot express -- so the schedule lives in the stateful `diffuse` +handler (it carries the step counter and advances), and ITERATE's fixed-point stop naturally coincides with the +cooled, sharpened cleanup converging. PnP needs no such trick: it is a TRUE fixed-point iterate (same +datafit+denoise body each step), the cleanest ITERATE fit. + +KEPT NEGATIVES: (1) the PROCEDURE TAX -- a noisy unbind-and-clean per instruction read makes the program form +slower than the direct Python loop; this is the price of being data, not a faster path. (2) the numerics never +leave NumPy -- datafit/denoise/diffuse are NumPy faculties behind APPLY; the FFT bind / SVD / Sinkhorn cannot +themselves become programs (they ARE the substrate -- circular). (3) B10's own negative travels: a BARE codebook +generation converges to a stored atom; feed a composed/continuous manifold for novel-but-valid samples. (4) the +capacity cliff still bounds program length (~32 instr at dim 1024, ~128 at 4096) -- these procedures are short +(1-2 body instructions under an ITERATE), well inside it. + +Tests: +2 (922 -> 924), test_restore_procedure_pnp_as_program + test_generate_procedure_diffusion_as_program in +test_integration.py. + +## Persistent topology: simplex budget + reused distance matrix (deployable speed) (shipped) + +Deployment feedback: persistent_topology was too slow for a rolling study (~4s/call on the user's windows, and +~32s on a 250-point cloud here). The cost was twofold: (1) the pairwise distance matrix was REBUILT at every one +of the 7 band scales (and again in _median_nn) -- 8x redundant; (2) on a DENSE cloud with no clean low-dim +topology (a market delay embedding is the canonical case), the VR complex EXPLODES at the wider scales -- ~120k +tetrahedra at the top of the band on a 250-point blob -- and the GF(2) reduction over them dominates. + +THE FIX (no accuracy change on the legitimate use case): the distance matrix is computed ONCE and threaded +through (`_median_nn`, `betti_at_scale`, `_build_complex` all take an optional precomputed D), and a SIMPLEX +BUDGET caps the triangle/tetrahedron enumeration (`tri_budget`/`tet_budget`, default 15000). A well-sampled +low-dim manifold's complex is sparse and never approaches the budget (a 250-point sphere peaks at ~6k tris / +~10k tets); a blob hits it at the wide scales. Hitting the budget IS the signal the cloud is not a clean +manifold there: that scale returns B1/B2 as None (unreliable) and is skipped, `histogram['dense_scales']` reports +how many band scales exploded, and if EVERY scale is too dense the result is said plainly as "dense (no clean +topology)" rather than a misleading Betti number. + +MEASURED: a 250-point Gaussian blob went 32.5s -> 0.39s (83x), with `dense_scales: 3` flagging that 3 of 7 band +scales were too dense to read. The four manifolds still classify correctly (line/circle ~0s, torus 0.16s, +sphere 0.17s) and the sine-embedding kept-negative still holds (it does NOT read as a clean ring). + +HONEST SCOPE (unchanged by this fix -- a speed/honesty fix, not a capability claim): persistent homology still +tracks the shape of a FIXED point cloud, not the dynamics of a 1-D signal -- on a market delay embedding its +B0 counts shadow volatility (point spread), which is why a sine fragments rather than reading as a loop. The +budget makes it FAST and makes it SAY when the cloud is a blob; it does not make it the right tool for 1-D +time-series regime detection. That remains a kept negative. + +Tests: +2 (924 -> 926), test_dense_cloud_is_capped_and_flagged + test_budget_does_not_change_clean_manifolds in +test_holographic_topology.py. + +## Fast topology promoted to a gate; SpectralBasis scaled with a Chebyshev partial eigensolver (shipped) + +Two things off the back of the persistent-homology speedup (83x, prior section). A performance audit profiled the +remaining faculties at REALISTIC sizes (not test sizes -- small inputs hide scale-dependent cost): Wasserstein/ +Sinkhorn early-stops and returns small histograms; the de-Doppler bank, Kuramoto sync, creature, and the learning +modules are all bounded or cached. Persistent topology was the lone heavy outlier (a unique combo of redundant +recompute + combinatorial explosion), and it was already fixed. The one latent O(n^3) left was SpectralBasis's +eigh. So: a dividend from the topology fix, and the eigh. + +THE GATE (the dividend). Now that naming a cloud's topology is sub-second even on a structureless blob, +persistent homology becomes a first-class GATE. `is_manifold(points)` runs `manifold_topology` and returns +{is_manifold, topology, betti, dense_scales}; is_manifold is True iff the cloud is ONE connected piece (B0 == 1) +and at most `max_dense_scales` band scales were too dense to read. It earns its keep on the spectral denoiser, +whose premise is a smooth field on a CONNECTED manifold -- exactly what the gate checks. `denoise(method= +'spectral', check_manifold=True)` runs the gate first and raises on a non-manifold (with an escape hatch: +check_manifold=False) rather than silently returning graph low-pass. MEASURED: the spectral map cleans a 2-sphere +field 3.74 -> 1.08, but on a random 4-D blob it barely moves (4.37 -> 4.20) -- the gate names which case you are +in for free. Default off keeps the path overhead-free and backward-compatible. + +THE EIGENSOLVER (the hard half) -- and the KEPT NEGATIVES of FOUR failed approaches before the one that works. +SpectralBasis built its modes with np.linalg.eigh: ALL n eigenvectors at O(n^3) to keep the lowest ~12. Fine to +~1500 points (0.6s), painful at 3000 (4.4s), worse at 5000 (22s). A partial solver should compute only the +smooth modes. The honest difficulty is DEGENERACY: a 2-sphere's Laplacian carries 2l+1 modes at each eigenvalue +(cumulative block boundaries are perfect squares 1,4,9,16,25), so a COUNT cutoff like n_basis=12 lands INSIDE a +degenerate block (l=3 spans modes 10-16) and the smooth subspace at that cutoff is itself ambiguous. Measured +failures, kept on record: + * Shifted Lanczos (M = sigma*I - L, top-k Ritz): the wanted modes cluster near sigma at the top of the shift; + single-vector Lanczos converges to the WRONG subspace (projector diff ~4, denoise 8.0 vs eigh 1.2). 14-28x + speed but useless. + * Block subspace iteration on the same shift: the smooth modes compress to a tiny relative spread near sigma + (convergence ratio ~1), so it never converges (projector diff ~2-3.5, denoise 8.5-15.8). + * Unshifted Lanczos on L, smallest-k Ritz: works at n=400 (0.92 == 0.92) but DEGRADES with n (n=1500: 2.48 vs + 1.37) -- single-vector Lanczos cannot capture a degenerate subspace, and the degeneracy worsens the larger + the cloud. + * Graph-Tikhonov low-pass x = (I + gamma*L)^-1 fn via CG (NO eigendecomposition): robust and fast (0.002-0.17s) + but a SOFT filter ATTENUATES the signal modes along with the noise (best 3.2 vs eigh 1.2). The field z^2-0.5x + lives in l<=2; a gentle filter that suppresses l>=3 also suppresses l<=2, so it cannot match a hard k-mode + cutoff. Fundamentally not a substitute -- a kept negative about soft vs hard spectral filtering. + * Nystrom (landmark eigh + kNN extension): preserves the hard cutoff on the subsample but the extension error + GROWS with n (1.67 at 800 -> 3.62 at 3000), i.e. it degrades exactly where eigh is too slow to use. Useless. + +THE METHOD THAT WORKS: Chebyshev-filtered subspace iteration (ChebFSI) -- what real sparse eigensolvers use. A +degree-d Chebyshev polynomial of [lambda_cut, lambda_max] stays bounded on that interval and grows fast BELOW +lambda_cut, so it AMPLIFIES the wanted low-eigenvalue subspace by orders of magnitude; a block subspace +iteration then converges THROUGH the degeneracy (the block captures degenerate modes; the filter gives a real +gap). lambda_cut is estimated by a few cheap Lanczos steps (eigenvalue ESTIMATES at the extreme converge even +when the VECTORS do not under degeneracy). The decisive piece is the SPARSE matvec: the kNN Laplacian has ~k +nonzeros per row, so v -> Lv is O(n*k) (verified byte-identical to the dense knn_laplacian, max diff 2e-14), +never forming the n x n matrix. Dense-matvec ChebFSI matched eigh EXACTLY (projector diff 0.00) but was barely +faster (the dense matvec is the bottleneck); the sparse matvec is what turns it into a real speedup. + +MEASURED (tuned oversample=14, outer=6, deg=24): projector diff to the exact eigh is 0.000 at n=1800, 0.013 at +3000, 0.310 at 4500 (denoise within ~2% throughout), with speedup GROWING as eigh's O(n^3) bites: 1.7x at 1800, +3.8x at 3000, 7.1x at 4500. SpectralBasis switches to ChebFSI above `partial_threshold` (default 2000) and keeps +the exact dense eigh below -- faster there AND bit-identical, so every existing test/selftest (small clouds) +is unchanged. + +KEPT NEGATIVES (travelling in the docstrings and tests): ChebFSI is an APPROXIMATION whose projector error grows +slowly with n; and it lifts the EIGH O(n^3), NOT the O(n^2) kNN distance build, which itself caps practical use +at a few thousand points -- a spatial index would be the next rung. Below threshold the exact eigh is used +(faster and exact). The is_manifold gate inherits manifold_topology's scope: it reads a WELL-SAMPLED manifold, +and a disconnected manifold (B0 > 1) reads as not-a-manifold by design (the gate wants one connected piece). + +Tests: +5 (926 -> 931). test_cheb_eigenbasis_matches_full_eigh_at_scale + +test_spectral_basis_thresholds_to_exact_below_cutoff in test_holographic_spectral.py; test_is_manifold_gate_faculty ++ test_spectral_denoise_check_manifold_guard + test_spectral_denoise_scales_to_large_cloud in test_integration.py. + +## Backlog triage + D1: the honesty discipline as a structural lint on protocol-vectors (shipped) + +A forwarded "VSA-program backlog" proposed porting a session of analysis methods into VSA programs. The +engine's rule is to ground against the LIVE code, not the proposal, and the audit was the usual humbling one: +most of it is already built. A1 (program convention + interpreter) is `holographic_machine.py` (the +stored-program VM) plus the mind's `learn_procedure`/`run_procedure`/`index_procedures`/`canonicalize_procedure` +layer. C1 (the "honesty harness keystone") is already `walk_forward_recall` -- the same six checks the backlog +describes (a shuffled-outcomes null that must collapse to chance, a persistence baseline the signal must beat, a +chance band, scale-correlation, net-of-cost). E3 (massive parallel scan) is `scan` (SPRT-per-channel + honest +per-channel-length FDR). A3 (the exact arbiter) is `RecallNull.pvalue` / `SPRTRecall.decide` / `bh_fdr`, and the +self-hosting audit already drew the boundary. So C1/E3/A1/A3 are shipped; the backlog author rebuilds the harness +by hand because they didn't know to call it `walk_forward_recall` -- the denoising/self-hosting lesson again. + +TWO THINGS THE TRIAGE FOUND WRONG, kept on record so they aren't rebuilt: + * A2 (the "superposed null engine," the backlog's claimed single biggest speed win) is the capacity cliff in + disguise. The null is ALREADY batched -- `RecallNull.fit` scores all n_null random queries in one matmul, + `(units @ Q.T).max(axis=0)` -- so there is no sequential loop to parallelize. And running N independent + computations in one superposed bundle and reading N exact scalars back is exactly the thing the engine has + measured cannot be done (a 2048-d bundle recalls ~100% of 64 items, ~0% of 2048); the crosstalk would land + in the null that must stay exact. The backlog even says "verify the superposed run equals N separate runs + first" -- the verification would just re-derive the cliff. + * The market half (B, D2, E1/E2) bets against a documented negative: SOL returns came back efficient-market- + like (shuffle-indistinguishable, survived 10x the data), and the dynamics operator only ties a trivial mean + predictor. Faster search over absent structure finds the absence faster. + +THE ONE GENUINELY-NEW, ON-MISSION IDEA -- and the one thing built (D1). Turn the honesty discipline from a habit +you maintain into a STRUCTURAL PROPERTY you can check. Because a protocol (an analysis procedure) is +program-as-data, its step structure can be READ BACK from its program vector (the VM's own unbind+cleanup), and +anti-patterns become structural queries. `holographic_protocol.py`: + * `build_protocol(machine, steps)` assembles an ordered list of faculty-step NAMES (encode, combination_search, + calibrated_null, fdr, oos_split, decide, ...) into one program vector. The names match real mind faculties + (recall, RecallNull, bh_fdr, walk_forward_recall), so a protocol is a real analysis program, not a toy. + * `protocol_role_sequence` decodes the program VECTOR position-by-position and maps each APPLY's faculty to a + ROLE (SEARCH / NULL / FDR / SPLIT / DECIDE / ENCODE / NEUTRAL) via a default, extensible taxonomy. + * `audit_protocol` evaluates three rules over the recovered structure: (R1) a SEARCH with no procedure-matched + NULL -- the canonical artifact-factory; (R2) a searched-and-scored FAMILY (SEARCH + DECIDE) with no FDR / + look-elsewhere control; (R3) selecting then scoring (SEARCH then a later DECIDE) with no out-of-sample SPLIT + between them. Returns {sound, roles, sequence, violations}. +Wired as the mind faculty `audit_procedure(steps=[...])` (or a prebuilt program+n_steps). + +WHY this is the SOUND version of D1 (and where the backlog over-reached): the backlog imagined checking "a whole +space of protocols in ONE operation" -- but reading a per-protocol property out of a SUPERPOSITION of protocols +is the same A2 capacity cliff. So that framing is dropped: the audit reads ONE protocol vector at a time, which +is genuinely holographic (structure recovered by unbind+cleanup) AND correct. + +MEASURED (the selftest IS the earns-its-place measurement): the protocol structure round-trips EXACTLY from the +program vector at protocol length (decode reliable for ~6-step protocols at dim 4096); a complete honest protocol +reads sound; each of the three anti-patterns is flagged on a protocol built to contain it; and a no-search +procedure (a restoration loop: datafit -> denoise) is NOT flagged -- targeted, not trigger-happy. + +KEPT NEGATIVES (in the docstrings and tests): it is a STRUCTURAL lint on DECLARED steps, not a data-flow analysis +-- the single-accumulator VM does not encode per-step data lineage, so "scores the exact rows it selected on" is +approximated by the ORDER check (no SPLIT between SEARCH and DECIDE), not by tracking data identity. The per-step +decode is bounded by the program vector's capacity, so a protocol must be SHORT to read reliably (the procedure +tax; longer protocols need a larger dim). And an unknown faculty name carries NO obligation (role NEUTRAL), so a +missing taxonomy entry fails OPEN (no false alarm), not closed. + +Tests: +8 (931 -> 939). test_holographic_protocol.py (selftest + structure-round-trips + complete-is-sound + +the three anti-pattern flags + no-search-not-flagged) and test_audit_procedure_faculty in test_integration.py. + +## D3: the findings registry -- a research log as a holographic knowledge structure (shipped) + +The second genuinely-new idea from the forwarded backlog (D3), built after D1 and measured the same way. A +research log that you query by similarity and that detects its OWN contradictions. The substrate was already +present -- the relations layer (`holographic_relations.KnowledgeStore`) encodes role-bound records and runs +explain / name / analogy-as-unbind on them -- so the only thing missing was the operation a research log +actually needs: contradiction detection. + +A FINDING is a structured claim: a SUBJECT affects an OBJECT with a POLARITY (+1 helps/strengthens, -1 +hurts/backfires), optionally under a CONDITION (a regime: a horizon, a session, an asset). It is encoded the way +every record in the engine is: `finding = bind(SUBJ, subject) + bind(OBJ, object) [+ bind(COND, condition)]`, so +the existing explain/analogy operations compose with it for free. `holographic_knowledge.FindingRegistry`: + * `add(subject, object, polarity, condition=None, note=None)` -- stores the structured record plus two + vectors: the FULL finding (subj+obj+cond, for query) and the CLAIM (subj+obj only, for tension pairing). + * `query(subject=, object=, condition=)` -- recall by similarity to a PARTIAL claim. Bundle the binds for the + given slots, cosine against findings. ROLE-SENSITIVE: object=momentum recalls findings where momentum is the + OBJECT, not where it is the subject (the dividend of structured encoding over a bag of words). + * `tensions(claim_tol=0.85)` -- THE HEADLINE. For every pair whose CLAIMS match (cosine of their subj+obj + bindings >= tol) and whose POLARITIES are opposite, report a tension, classified FLAT (same/absent condition + -- genuinely conflicting, one must be wrong) or CONDITIONED (different conditions -- reconcilable, the + outcome is conditioned on the differing dimension). + +The backlog's exact example works: "ER strengthens momentum at 10d" (+1, horizon_10d) vs "ER backfires intraday" +(-1, intraday) is flagged CONDITIONED (reconcilable), while a planted "bracket convex" (+1) vs "bracket drift +masquerades as convexity" (-1), both unconditioned, is flagged FLAT (must resolve). + +THE EXACT-DOOR DISCIPLINE (the engine's rule, again): RETRIEVAL is holographic -- the cosine over the bound claim +finds candidate conflicts -- but the VERDICT is EXACT: the polarity sign and the condition equality decide. With +unitary atoms two identical claims give claim-cosine 1.0 and "same subject, different object" gives ~0.5, so +claim_tol=0.85 cleanly requires BOTH subject and object to match (different objects are different claims, not +contradictions). Wired as the lazily-cached mind faculty `finding_registry()`. + +MEASURED (the selftest IS the earns-its-place gate): query by subject/object recalls the right findings and is +role-sensitive (a finding with the token in the SUBJECT slot is NOT matched by an OBJECT query); the conditioned +tension and the flat contradiction are each classified correctly; and exactly the two genuine tensions are found +-- no false positives from random similarity, and same-polarity findings about the same claim are NOT flagged +(they agree). + +KEPT NEGATIVES (in the docstrings and tests): findings are STRUCTURED claims, NOT free prose -- turning a +2300-line narrative log into structured claims is an NLP step this engine does not do (no embeddings, no parser); +that is the manual / future-LLM boundary, stated plainly. And the tension scan is O(n^2) pairwise claim-cosine -- +fine for a few thousand findings; a HoloForest pre-filter is the standard sublinear answer at larger scale, +noted but not needed yet. + +Tests: +8 (939 -> 947). test_holographic_knowledge.py (selftest + query-by-subject + role-sensitivity + the +flat-vs-conditioned classification + no-false-positives + same-direction-not-a-tension + signed-polarity) and +test_finding_registry_faculty in test_integration.py. + + +## D3 made durable: the findings log persists by storing claims, not vectors (shipped) + +A research log that evaporates when the session ends is half a tool. D3's FindingRegistry now saves and loads -- +but the design follows the determinism rule the whole engine runs on, rather than the obvious route of pickling +the vectors. + +THE DESIGN. The saved file holds ONLY the structured claims (subject, object, polarity, condition, note) plus the +dimension and the seed. It does NOT hold a single vector. The vectors -- the bound finding vectors used for recall +and the claim vectors used for tension pairing -- are a deterministic function of the claims and the seed, so on +load they are REBUILT by simply re-adding each finding. This is the demoscene move the engine uses everywhere +(reproduce the structure from the seed instead of storing it): the file is tiny (a JSON list of claims), and the +restored registry is not an approximation of the original but bit-for-bit the same object. + +MEASURED (the selftest and tests are the earns-its-place gate): after save -> load, the findings list is identical, +every rebuilt finding vector and claim vector is np.array_equal to the original's (so recall and tension verdicts +are not merely close but exact), and the conditioned-vs-flat tension classification reproduces. A test asserts the +file contains NO vector data (only dim/seed/findings). And a "keeps growing" test reloads a log, adds an +opposite-polarity finding under a different condition, and confirms the new conditioned tension is detected against +the restored findings -- the durable-log use case end to end. Through the mind, m.finding_registry().save(path) +persists the session's log and FindingRegistry.load(path) restores it standalone. + +WHY IT MATTERS HERE. This shipped alongside an image-generation push (VSA-diffusion generate_structure rendering +novel valid scenes, and the splat sharpness finding that the right splat scale is content-dependent -- small +splats sharpen edges but hurt smooth fills). Those findings were logged into D3, which surfaced the genuine +conditioned tension (small_splats -> fidelity, +1 at sharp_content vs -1 at smooth_content), and D1 audited the +splat-config TUNING procedure (flagging tune-and-report without a held-out split). Persistence is what lets that +accumulated, reconciled knowledge outlive the conversation it was measured in. + +Tests: +4 (947 -> 951). test_holographic_knowledge.py (save/load round-trips findings+tensions+query; +load-rebuilds-vectors-from-seed-not-file with the no-vectors-in-file assertion; loaded-registry-keeps-growing) and +test_finding_registry_persists_across_minds in test_integration.py. + + +## Holographic vector-graphics (SVG): the sharp, resolution-independent cousin of the splat archive (shipped) + +The splat work kept fighting one thing: a Gaussian basis BLURS sharp edges, so a crisp square needs smaller splats +(which then hurt smooth content) or supersampling (which spreads a fixed splat budget too thin). The fix turned out +to be not a better Gaussian but a different primitive. An SVG // has analytically EXACT +edges at any zoom -- the rasteriser computes exact coverage from the maths -- so representing/generating images as +vector primitives makes the sharpness problem disappear, and makes the result resolution-INDEPENDENT for free. + +This is a structural match the engine already had, the same way a splat scene is a bundle: a vector-graphics scene +is ALSO a bundle of role-bound primitives. holographic_svg.py's HolographicSVG encodes a scene -- a list of +(type, x, y, size, colour) primitives -- into ONE hypervector: each primitive is bundle(bind(TYPE,t), +bind(X, encX(x)), bind(Y, encY(y)), bind(SIZE, encS(s)), bind(COLOUR, c)), and the scene is the bundle of those +primitives each bound to a SLOT. Discrete attributes (type, colour) decode by cleanup; continuous ones (position, +size) by the ScalarEncoder's grid decode -- the continuous analogue of cleanup. SVG emission is pure string +formatting (no new dependency -- NumPy and Flask only). + +MEASURED (the selftest and tests are the earns-its-place gate): +- ROUND-TRIP: a 3-primitive scene encodes to one vector and decodes back with type and colour EXACT and + position/size within ~0.016 on [0,1] (at dim 4096) -- a faithful content-addressable picture. +- MORPH AS ARITHMETIC: interpolating two scenes' HYPERVECTORS and decoding the blend matches a direct parameter + lerp at the midpoint to within ~0.014. So (1-t)*vA + t*vB really does interpolate the picture -- vector + arithmetic in the holographic space, rendered crisp through SVG. This is the same morph the splat experiment did + in pixel space, now exact and resolution-free. +- GENERATE: the composed-manifold diffusion (generate_structure, the B10 sampler) runs over a discrete primitive + codebook (type x grid-cell x colour) and produces 6/6 distinct novel scenes, valid by construction, rendered + crisp. +- RESOLUTION INDEPENDENCE (shown in the figure): the same scene stored as a 48px raster and upscaled is visibly + blocky; rendered from the SVG at 480px it is razor-sharp. No splat blur, no supersampling. + +Wired as the svg_canvas() faculty on UnifiedMind (cached, built at the mind's dim/seed), beside the generative +faculties. KEPT NEGATIVE / SCOPE: primitives are isotropic (one size, a palette colour) -- anisotropic +width/height, rotation, gradients/strokes, and bezier paths are the honest next step, the same boundary the +anisotropic-splat work drew. And round-trip fidelity scales with dimension (a few primitives are faithful at +2048+; a crowded scene wants more -- the bundle's finite capacity, shown not hidden). + +The through-line: the engine generates compositional structures well, and SVG is simply the SHARP, resolution- +independent renderer that compositional generation deserved -- the sharpness ceiling the splat work measured was a +property of the Gaussian basis, not of the engine, exactly as the matmul and splat-sharpness corrections kept +showing (a quality question is empirical, not a structural wall). + +Tests: +8 (951 -> 959). test_holographic_svg.py (selftest + round-trip + morph-midpoint + generate-diverse-and- +deterministic + well-formed-SVG + geometry-scales-with-size + too-many-primitives-rejected) and +test_svg_canvas_faculty in test_integration.py. + + +## Learned-energy generation: a measured negative (the EP autoencoder denoises but does not generate) + +The SVG modality closed with an obvious-looking next lever: swap the hand-built grid codebook the composed-manifold +diffusion uses for a manifold LEARNED from data -- use LearnedEnergyMemory (the EP autoencoder from the June-24 +work) as the diffusion denoiser, so generation samples a learned manifold. Probed it; it does not work, and the +measurement says clearly why. NOT SHIPPED. + +WHAT WAS MEASURED. +- On the established 2-D bump manifold (2000 samples, the selftest's working config), the learned energy DENOISES + near-manifold points with ~0.43 relative error -- a RELATIVE win over a matched-byte codebook (the kept positive) + but modest in absolute terms. From PURE noise a single cleanup lands ~0.51 OFF the manifold; a Langevin walk + (seed on the manifold, iterate add-noise -> cleanup) does best -- ~0.33 off, 30/36 latent cells covered, novel + (0.32 from any stored sample) -- but still imprecise. +- On a LOW-DIM SMOOTH manifold (a 9-D scene-parameter family: three circles in a cluster whose centre moves with + two latents) it FAILS outright. The decisive test: the raw AVERAGE of two nearby training scenes is already + on-manifold (0.014-0.017) -- the manifold is locally convex, so plain interpolation generates a valid in-between + scene -- and the learned-energy cleanup makes it WORSE (0.32-1.12) at every bottleneck size (n_hidden in 2,3,4). + A figure (learned_svg_generation.png) shows it: clean clusters in, a single collapsed/distorted blob out. + +WHY (the load-bearing lesson). A good DENOISER is not a good GENERATOR. Denoising only needs a small local +correction from a point already near the manifold; generation needs the energy's MINIMA to lie ON the manifold so +that descending from afar lands on it. A single-noise EP autoencoder's free-state attractors are not reliably on +the manifold -- they collapse toward a smeared mean -- so it denoises (locally) yet distorts when asked to generate. + +THE CONSTRUCTIVE OUTCOME. +- For COMBINATIONAL generation the engine's right tool is the composed-manifold diffusion (generate_structure), + already shipped (9/9 distinct valid SVG scenes). +- For CONTINUOUS in-style variation, simple INTERPOLATION in the composed/parameter space already works (the raw + midpoint is on-manifold to 0.014) -- which is exactly what the shipped SVG morph does. No learned energy needed + on locally-convex manifolds. +- A learned GENERATIVE manifold (for genuinely non-convex cases) would need a denoiser trained ACROSS noise levels + -- a real diffusion model, not this single-noise autoencoder. Filed as backlog VG-2. + +The kept negative travels in LearnedEnergyMemory's docstring (a SCOPE paragraph: denoiser, not generator) so a +future caller meets it at the point of use. No test/count change -- nothing shipped; the negative is the result. + + +## Splat edge artifacts are under-reconstruction (density + greedy fit), fixed by a joint refit -- not a basis floor (measured, corrected) + +This entry CORRECTS a too-quick conclusion. The question was splat edge blur, and the first pass blamed two things; +only one held up, and the panel's 3D-graphics seat (and the actual 3DGS literature) pointed at the real cause. + +WHAT STILL HOLDS: SUPERSAMPLING A FIXED SPLAT SET IS A NO-OP. Render the same fitted splats direct@N vs +8x->downscale: coarse 14.89 -> 14.88 dB, fine 20.95 -> 20.82, edge PSNR identical, visually indistinguishable. A +sum of Gaussians is band-limited -- there is no high-frequency content to alias, so area-averaging equals point- +sampling. Supersampling fixes ALIASING, which a Gaussian sum does not have. (It DOES become relevant once splats go +SUB-pixel -- then point-sampling aliases them -- which is the last-mile of edge sharpness and where the SVG modality, +with analytic exact-coverage edges, is simply the better tool.) + +WHAT WAS WRONG: calling the residual blur/lumpiness a "basis floor" ("a smooth basis cannot represent a hard edge"). +At a FINITE output resolution the AA target is itself band-limited, and a sum of Gaussians can approximate it +arbitrarily well with enough density -- so the artifacts were UNDER-RECONSTRUCTION, not a basis limit. Two causes, +both fixable: (1) too few splats (K=140 for a 4096-pixel image is sparse -- the exact under-reconstruction adaptive +density control targets); (2) GREEDY matching pursuit fits each amplitude against the residual at placement time, so +overlapping splats systematically double-count, and it never goes back to fix it. + +THE FIX, MEASURED AND SHIPPED: `splat_refit` -- one JOINT least-squares solve over all amplitudes (positions/scales +fixed), the "looping" step. On the real engine fit it adds ~2-4 dB (square+blob 23.64 -> 27.11 at K=400; rings 33.27 +-> 35.74), and the gain GROWS with the splat count (more overlap to disentangle). It is closed-form and gradient-FREE, +so it stays inside the NumPy-only rule -- distinct from the gradient optimisation of positions/scales/opacity that +full 3DGS does (that needs autodiff and stays out of scope). Wired as `splat_fit(..., refit=True)` and defaulted ON in +the `splat_field` faculty. + +THE 3DGS GROUNDING (web): adaptive density control is the core of 3D Gaussian Splatting -- clone/split Gaussians in +under-reconstructed (high-error) regions, prune elsewhere, iterating throughout optimisation, with the number of +Gaussians set automatically by image complexity. The amplitude refit is the gradient-free half of that loop; true +clone/split densification + position/scale optimisation is the natural next step (backlog), needing the autodiff the +project avoids -- the existing gradient-free `aniso_fit` is where to push it. + +THE LESSON (again): a quality artifact is empirical, not a structural wall. "Smooth basis can't do edges" was the same +kind of premature-floor claim the matmul and splat-sharpness corrections already caught; measuring it found a real, +shippable fix instead. + +Tests: +4 (959 -> 963). test_holographic_splat.py (joint_refit_beats_greedy_and_gain_grows_with_count, +splat_fit_refit_flag_matches_manual_refit, splat_refit_handles_empty) and test_splat_field_joint_refit in +test_integration.py. + +## Low-discrepancy sampling: even coverage where random clumps and a grid aliases (shipped) + +The rendering-engine lessons arc opens at its cheapest, broadest-backed item (the panel's pick): a low-discrepancy +sampler. The recurring graphics fact -- random points clump into holes, a regular grid aliases, and the +blue-noise / low-discrepancy middle ground is what production renderers actually sample on (Pharr's PBRT sampling +chapter; Roberts 2018) -- applies anywhere the engine PLACES points to COVER rather than to draw an INDEPENDENT +sample: generation seeds, codebook / anchor placement, the sub-pixel jitter a temporal-accumulation pass will need. + +`holographic_lowdiscrepancy.low_discrepancy(n, d, seed)` is Roberts' generalised R-sequence -- the d-dimensional +golden-ratio / plastic-constant additive recurrence, one line of NumPy, no state, deterministic, and PROGRESSIVE +(any prefix is itself well-distributed, so you can keep taking points). Measured: 64 points cover ~28% tighter than +the mean of random (dispersion 0.16 vs 0.23), and as a quasi-Monte-Carlo integrator the same points estimate a +smooth integral with ~13x less error than plain Monte Carlo at equal count (0.0011 vs 0.0143) -- the downstream +payoff of even coverage, not just a prettier scatter. Wired as the `low_discrepancy_sample` faculty (defaults to +the mind's seed). KEPT SCOPE: this is a COVERAGE tool; where genuine independence is wanted (bootstrap, noise +injection) default_rng stays -- these points are correlated by construction. It is the sampler the later +jitter-accumulate (ACCUM-1) and anchor-placement (CACHE-1/3) backlog items will draw on. + +Tests: +2 (963 -> 965). test_holographic_lowdiscrepancy.py (the module _selftest: coverage beats random, QMC beats +MC, deterministic + progressive) and test_low_discrepancy_sample_faculty in test_integration.py. + +## Throughput-gated traversal: Russian roulette for holographic paths (shipped) + +The second rendering-engine-lessons item, and the one with the broadest cross-seat backing on the panel. The +identity behind it -- in the FFT/phasor domain a bind is elementwise complex MULTIPLICATION, so a chain of +binds is a running PRODUCT of per-step transfer functions, exactly a ray's THROUGHPUT -- means a holographic +traversal (a multi-hop recall, the resonator's iterative peeling, a recursive descent) is a ray bouncing +through the space, its recoverable signal attenuating until every further step is noise. Path tracers terminate +such a path with Russian roulette once its throughput is negligible; this ports that move. + +`holographic_traverse.gated_traverse(step, start, floor, max_steps, min_steps)` drives a step function -- +step(state) -> (next_state, throughput, payload) -- with a cheap running confidence (a cleanup cosine, a +convergence margin) and STOPS the instant it falls below the floor, abstaining on that step (not recording +noise). Measured on a directed linked list stored in superposition: the gate recovers every valid hop and then +abstains the moment the chain is exhausted (signal gone) -- the correct prefix [1..10] in order, stopping where +the past-end unbind is noise (throughput 0.03 vs ~0.3 for valid hops), i.e. 10 steps vs a fixed depth of 30. +Crucially it needs NO ground truth: the cheap confidence tracks the true recoverable cosine closely enough to +know when the ray has gone dark. Wired as the `gated_traverse` faculty (between the recall and resonator +faculties it serves). + +KEPT NEGATIVE / SCOPE: the gate keys on LOW confidence (the ray dark); it does NOT catch a CONFIDENT-but-WRONG +step -- the capacity-ambiguity regime where crosstalk returns a wrong atom at moderate confidence -- which is a +calibration problem (the calibrated-null / MIS items), not a throughput one. And this is the deterministic FLOOR +(right for FOLLOWING a path); the unbiased STOCHASTIC Russian roulette (terminate with prob 1-T, boost survivors +by 1/T) is for ACCUMULATING a sum and is a separate, not-yet-measured extension. The directed chain reused the +RAY-3 lesson in passing -- a bundle of bind(x_i, x_{i+1}) is undirected, so the links are permuted (a direction +role) to make traversal unambiguous. + +Tests: +2 (965 -> 967). test_holographic_traverse.py (the module _selftest: gating logic on a known profile, +and the real directed-chain traversal) and test_gated_traverse_faculty_recovers_chain_then_abstains in +test_integration.py. + +## Adaptive splat count: sample to a noise floor, not a budget (shipped) + +The third rendering-engine-lessons item: V-Ray's adaptive sampler (and 3DGS densification in spirit) ported to +splats. A path tracer doesn't spend a fixed number of rays per pixel -- it spends until the variance is below a +noise floor, few where the image is smooth and many where it is busy. The splat fitter previously took a fixed K +regardless of content; this makes the COUNT adaptive. + +`holographic_splat.adaptive_fit(target, noise_thresh, k_min, k_max, refit)` runs the same matching pursuit as +splat_fit but stops once the residual RMS falls below noise_thresh * the target's range (bounded [k_min, k_max]), +returning (splats, k_used). Exposed through the existing `splat_field` faculty as a `noise_thresh` argument +(default None keeps the fixed-k path byte-for-byte). Measured at noise_thresh=0.03: a one-blob field fit to 33.2 +dB with 13 splats, a seven-blob field to 33.0 dB with 36 -- matched quality, count tracking content -- where a +fixed k=20 over-spends on the easy field (36.5 dB, splats wasted) and starves the busy one (27.0 dB). The count +is orthogonal to the joint refit: the count is WHERE the splats go, the refit is HOW STRONG they are. + +KEPT CAVEAT (in the docstring and a test): the threshold gates the GREEDY residual, so quality is only +APPROXIMATELY equalised; and a HARD-EDGED target the smooth isotropic basis cannot represent simply runs to k_max +rather than converging -- the adaptive count is meaningful only for fields the Gaussian basis can actually fit. +The gradient optimisation of positions and anisotropic covariances (full 3DGS) still needs autodiff and stays out +of scope; this item moves only the COUNT. + +Tests: +3 (967 -> 970). test_holographic_splat.py (adaptive_fit_count_tracks_content_at_matched_quality, +adaptive_fit_respects_bounds) and test_splat_field_adaptive_count in test_integration.py. + +## SHARP-1: a Mitchell-Netravali reconstruction kernel does NOT sharpen the scalar decode (measured negative) + +The rendering-lessons backlog's SHARP-1 proposed giving the ScalarEncoder a Mitchell-Netravali reconstruction +kernel: the encoder is a Fourier phase encoder whose similarity kernel is the characteristic function of its phase +distribution (Bochner), already shipping rbf (Gaussian phases -> all-positive, blurs) and sinc (uniform phases -> +the ideal band-limited reconstruction filter, sharp but it rings). Mitchell is the production reconstruction filter +that lives BETWEEN those two -- band-limited negative lobes like sinc, but with the ringing tamed. The hypothesis: +its negative lobes would sharpen the scalar decode, the same negative-lobe sharpening the splat joint-refit already +exploits. Prototyped it thoroughly; it does not yield a measurable win. NOT SHIPPED. + +WHAT WAS MEASURED. +- The Mitchell kernel IS realisable in this encoder: its frequency response (the phase distribution it needs) is + non-negative to 0.2% (clip the tiny negative lobes, sample phases from it). kernel_at then matches the Mitchell + cubic by Bochner. So the kernel exists and is correct -- that part worked. +- At matched main-lobe width it sits exactly where theory says: peak side-lobe (ringing) 0.022 vs sinc's 0.166 vs + rbf's 0.008 -- the reconstruction-filter compromise, confirmed. +- But DECODE accuracy is a TIE across all three kernels: single value, value-under-noise (sigma 0.3-1.0), and a + value bundled with 0-8 distractors all land within noise of each other. The decode is an argmax over a fine grid + -- a peak DETECTION -- and argmax is insensitive to side-lobe shape, the only thing the kernel choice changes. So + the negative lobes have nothing to grip. +- A multi-value DENSITY read-out (a SUM, where side-lobes DO shape the output) is messy and does not favour + Mitchell: sinc's ringing corrupts it badly (1 peak recovered for 3-7 separated values), rbf is adequate, and + Mitchell's own residual ringing OVER-counts on structured inputs (10 peaks for 5 equally-spaced values). On + random dense sets Mitchell (3.12 mean peak-count error) only ties rbf (3.63) -- both poor -- so it does not beat + the simplest existing kernel. + +WHY (the load-bearing lesson). Negative-lobe reconstruction-filter sharpening helps where you SUM the kernel and +the OUTPUT IS that sum -- image reconstruction, where the splat joint-refit's ~51%-negative amplitudes measurably +sharpened edges. The scalar decode is the opposite kind of operation: a peak DETECTION (argmax), which reads only +where the main lobe is highest and ignores the side-lobes entirely. The rendering lesson is real but DOMAIN-bound +to reconstruction, not detection -- and the encoder already brackets the genuine sharpness/ringing tradeoff with +its existing rbf and sinc kernels. + +THE CONSTRUCTIVE OUTCOME. +- No new kernel is wired: per the project's own rule an option earns its place by measurement, and Mitchell earns + none here (ties rbf at best, regresses on structured density read-outs). The rbf/sinc pair already spans the axis. +- The place negative-lobe sharpening DID pay is reconstruction -- the shipped splat joint-refit -- so its natural + generalisation (SHARP-2: a tunable negative-lobe sharpening for the splat/image reconstruction path) is the branch + of this lesson worth pursuing, not the scalar encoder. + +No test/count change -- nothing shipped; the negative is the result. + +## Directed structure: a permutation direction role for sequences and graphs (shipped) + +The fourth rendering-lessons item -- RAY-3, the one Plate's seat argued to pull early for substrate-correctness. +A memory of edges bundled as bind(x_i, x_{i+1}) is UNDIRECTED: unbinding by a node returns BOTH neighbours, +predecessor and successor, at equal strength (measured ~0.33 vs ~0.33 at the operating dimension), so a traversal +cannot tell forward from backward -- the "predecessor leak". The engine's existing chain_structure (B7) carries +exactly this leak and relies on holographic_peel's per-peel history-aware cleanup to suppress it at decode time. + +RAY-3 fixes it at ENCODE time. Binding the successor through a fixed PERMUTATION first -- bind(x_i, perm(x_{i+1})) +-- breaks the symmetry: unbinding by x_i and undoing the permutation recovers the successor (~0.34), while the +predecessor term lands in the permuted subspace as noise (~0.00). The permutation does at encoding time what the +peel cleanup does at decoding time. It also generalises past linear chains: any set of directed EDGES bundles the +same way (a graph), and a branching node returns its whole successor set from one unbind (a 0 -> {1,2,3} node +hands back all three, ~0.40 each, cleanly above the non-successors). + +`holographic_directed` ships build()/encode_directed (M = superpose bind(node_i, perm(node_j))), successors() +(perm_inv . unbind . cleanup, with topk / thresh for branching), and make_step() (a gated_traverse-ready +closure). Wired as three faculties: directed_structure (build a sequence or graph), directed_successor (one +forward hop), and directed_traverse (a forward walk gated by recovery confidence -- the RAY-3 substrate under the +RAY-1 throughput gate, so the directed chain and the Russian-roulette walk compose into a clean forward traversal +that stops when the chain runs out). + +NOTE on the landscape: the permutation-direction-role mechanism already lived inside sequentiality_z's order test +(bind(a, permute(b,1)) as its transition model), but it was buried in a statistical probe, not exposed as a +general directed encoding; and SequenceMemory is a different representation entirely (POSITIONAL -- each element +rotated by its absolute position, for "what's at position i" / "does A precede B"), not edge/transition based. +RAY-3 is the additive, first-class directed-EDGE structure -- traversable, graph-capable, gated. + +Tests: +3 (970 -> 973). test_holographic_directed.py (the module _selftest: the directed-vs-undirected +predecessor-leak contrast, graph branching, gated-walk composition) and +test_directed_structure_forward_only_and_graph + test_directed_traverse_walks_chain_forward in test_integration.py. + +## Multiple Importance Sampling: Veach's balance heuristic for combining estimators (shipped) + +The fifth rendering-lessons item -- MIS-1, and the first genuinely NEW machinery in the arc rather than a port. +The engine has several estimators of the SAME quantity that each win in a different regime: exact 1-NN (Bayes- +optimal on discrete atoms), the soft dense-Hopfield blend (wins on continuous off-grid values, the B1 kept +negative), the manifold projection (smooth low-rank data), the forest (sublinear/approximate). Until now you PICK +one by hand. Veach's MIS combines them, weighting each by its per-query reliability. + +The load-bearing WARNING MIS encodes -- and the thing this module MEASURES -- is that NAIVELY AVERAGING estimators +reliable in different regimes makes things WORSE: the average carries each estimator's error into the other's +regime, landing below the better single. On a coarse sharp-kernel ScalarEncoder manifold with a 50/50 mix of +on-grid + off-grid cues, naive averaging of hard 1-NN and soft Hopfield scores error 0.0061 -- worse than soft +alone at 0.0040. The BALANCE HEURISTIC (w_i = r_i / sum_j r_j, the per-query reliability) lands at 0.0037, beating +both singles AND the naive average. + +`holographic_mis` ships combine_estimators(pairs, power) (the Veach balance/power heuristic primitive: pairs of +(estimate, reliability), w_i = r_i^power / sum r_j^power, power=1 balance / 2 power) and mis_recover(q, codebook, +beta, power) (combines hard 1-NN and soft Hopfield per-query, reliability = the cosine distribution's peakiness: a +sharp single winner trusts the exact atom, a close runner-up trusts the interpolating blend). Wired as two +faculties: combine_estimators and mis_recover. + +SCOPE / KEPT NEGATIVE: MIS beats EVERY single only in the CROSSOVER regime where neither estimator dominates. When +one dominates the whole regime (e.g., a very sharp kernel where soft wins almost everywhere), MIS MATCHES that +dominant estimator within a few percent rather than beating it -- mixing in the weak one costs a little. The +ALWAYS-TRUE win is over NAIVE AVERAGING; the win over the best single needs a genuine mix -- which is exactly the +MIS property: its value is when no single strategy is uniformly best. + +Tests: +2 (973 -> 975). test_holographic_mis.py (the module _selftest: naive-averaging-worse-than-best, MIS beats +naive and both singles, on the mix) and test_mis_recover_beats_naive_average_and_singles in test_integration.py. + +## Gradient-cached decode: Ward's irradiance gradients for smooth maps (shipped) + +The seventh rendering-lessons item -- CACHE-1, and the second of Group B's "combining & caching" pair (MIS was the +first). The engine evaluates smooth maps (a manifold decode, a splat field), and the naive dense read is a fine +grid + nearest-neighbour snap (the decode argmax, piecewise constant). Greg Ward's irradiance caching does better: +store the value AND its local gradient (Jacobian) at SPARSE anchors and interpolate FIRST-ORDER -- each anchor +extrapolates its own linear model v_i + J_i.(q - a_i) to the query, blended by 1/distance. + +MEASURED on a smooth splat / Gaussian-mixture field (analytic gradients): first-order gradient interp cuts error +~28% at a fixed 25 anchors vs the nearest-neighbour baseline (0.135 vs 0.189), and first-order @25 anchors roughly +MATCHES nearest-neighbour @49 -- gradients ~HALVE the anchor count a smooth decode needs. + +KEPT NEGATIVE (the load-bearing part): the blend MUST be local. A naive GLOBAL weighting (every anchor contributes, +weight ~1/distance, no cutoff) lets a distant anchor dump a wildly wrong long-range linear extrapolation into the +query -- measured ~2.7x WORSE than the local version (0.363 vs 0.135). This rediscovers exactly why Ward's +irradiance caching carries a validity radius + neighbour clamping. So the borrowable unit is the whole PACKAGE: +sparse anchors + stored gradients + a validity-radius locality guard. + +`holographic_cache` ships gradient_cache(anchors, values, jacobians) (scalar OR vector fields), gradient_cache_fd +(build from a field function alone via central finite differences), and interp_first_order(cache, q, +validity_radius, global_weights=False) (Ward first-order interp with the validity-radius guard; global_weights=True +exposes the negative). Wired as two faculties: gradient_cache and cache_interp. + +Tests: +2 (975 -> 977). test_holographic_cache.py (the module _selftest: gradients beat nearest-neighbour at fixed +anchors, ~halve the count, and global weights fail) and +test_gradient_cache_first_order_beats_nearest_and_global_weights_fail in test_integration.py. + +## Robust accumulation: harmonic weights + firefly clamping (shipped) + +The eighth rendering-lessons item -- ACCUM-2 and ACCUM-3, two cheap robustness fixes for the engine's averaging +paths (consolidation over a growing store, HoloForest vote-averaging, any iterate-and-average). + +ACCUM-2 (harmonic weights, TAA's lesson). A fixed-alpha exponential blend x <- (1-a)x + a*sample never fully +converges -- it keeps forgetting old samples, so its variance plateaus. The harmonic (1/n) running average +x <- x + (sample - x)/n weights every sample equally and converges. MEASURED on a stationary noisy stream: harmonic +error falls with N (0.0073 @ N=50 -> 0.0012 @ N=200 -> 0.0004 @ N=800), while the fixed-alpha EMA flatlines at +~0.034. KEPT CAVEAT: on a DRIFTING target the forgetful EMA tracks BETTER (0.031 vs harmonic 0.043) -- so +schedule='ema' stays available for non-stationary accumulation. + +ACCUM-3 (firefly clamping, V-Ray's adaptivity clamp / TAA history rectification). One outlier estimate (a firefly +recall/vote with a huge magnitude) skews a mean. Clamping each sample's deviation from the MEDIAN to k robust-scales +(the median deviation) winsorizes the outliers. MEASURED: with 5 injected fireflies, plain mean error 0.0467 vs +clamped 0.0004 (~100x more robust); on clean data, clamped == plain (no loss). + +`holographic_accumulate` ships robust_accumulate(samples, schedule, alpha, clamp_k) (schedule 'harmonic'/'ema'/'mean' ++ optional firefly clamp_k -- the two compose), plus harmonic_accumulate and clamped_accumulate conveniences. Wired +as one faculty: robust_accumulate. NOT forced into consolidation/forest internals (that would risk a regression); +shipped as the available robust accumulator for those paths, demonstrated on the canonical stationary-stream and +firefly cases. + +Tests: +2 (977 -> 979). test_holographic_accumulate.py (the module _selftest: harmonic converges + EMA plateaus + +drift caveat + firefly clamp robust/no-loss) and +test_robust_accumulate_harmonic_converges_and_clamp_resists_fireflies in test_integration.py. + +## Denoise-by-downscale: find a pattern by coarsening until noise averages out (shipped) + +XDATA-1, the entry point of Group G (the cross-data-type through-line) and the first rendering lesson that is +explicitly NOT about images. The lesson: "patterns can be found by downscaling to eliminate noise." Downsampling an +image pools neighbouring pixels so independent noise averages out while structure survives -- a MANIFOLD operation, +not an image one. The engine already owns its forms: consolidation (low-rank SVD) is downscaling for CORRELATED +VECTORS (pool across samples; the shared subspace reinforces, per-coordinate noise cancels), and low-pass filtering +is downscaling for SIGNALS. + +MEASURED on two non-image data types: +- LOW-RANK: a rank-3 subspace INVISIBLE in any single noisy vector (per-sample subspace energy ~0.03) is recovered + by pooling many samples -- subspace overlap grows with the sample count (0.22 @ N=100 -> 0.91 @ N=2000), the + averaging mechanism. (Requires the signal above the SVD/BBP recovery threshold; below it, fails safe -- nothing.) +- LOW-FREQUENCY SIGNAL: slow sinusoids buried under 2x noise (full-res corr 0.47) recovered by keeping the top-k + spectral components -- corr 0.90 to the clean signal. + +THE HONEST PART (fail-safe detection): keeping the top-k components ALWAYS concentrates a little, even on pure noise +(you select the largest of many random components -- the FFT noise concentration was 0.10 vs a uniform 0.023). So +"a pattern was found" is NOT read off the concentration; it is decided against a PERMUTATION NULL (shuffle to +destroy the structure, keep the noise level, recompute the score). Signal scores land ~60 sigma (low-rank) / ~14 +sigma (signal) above the null; pure noise lands AT the null -> found=False. The faculty does not hallucinate a +pattern in noise. + +`holographic_downscale` ships downscale_lowrank (SVD subspace), downscale_lowfreq (top-k FFT), and +find_pattern_by_downscale(data, kind='vectors'/'signal', k, n_null, seed) -> PatternResult(pattern, score, +null_mean, null_std, found). Wired as one faculty: find_pattern_by_downscale. + +Tests: +2 (979 -> 981). test_holographic_downscale.py (the module _selftest: recover buried subspace + buried +sinusoids, both found; pure noise of either type -> nothing) and +test_find_pattern_by_downscale_recovers_buried_pattern_and_noise_fails_safe in test_integration.py. + +## Looping denoise as diffusion on an arbitrary manifold (shipped) + +XDATA-2, the diffusion half of Group G. "A looping denoising process": iterate a denoiser and it walks onto the +manifold (DENOISING) or, from pure noise, walks ONTO it (GENERATING) -- the same operation in two regimes (B10). +The engine already ran this over the discrete CODEBOOK (hopfield.generate); XDATA-2 generalizes it to a LEARNED or +COMPOSED manifold given as a point cloud (a curved manifold, or a consolidation subspace from +find_pattern_by_downscale -- the two halves of Group G compose). + +The denoiser is a dense-Hopfield step over the manifold's samples: x <- softmax(beta * S.x) @ S (a soft move toward +the local samples). Iterating settles a point onto the manifold; annealing beta UP while injecting DECREASING noise +turns it into a diffusion sampler. + +MEASURED on a curved manifold (a unit RING in R^D -- the case where interpolation provably leaves the manifold): +- IDEMPOTENT DENOISE: a noisy ring point settles from ring-distance 0.59 to 0.029 and stays there (further steps + flat). The 0.029 floor is the sample-spacing limit (N=48 discrete samples), not error. +- BEATS INTERPOLATION: the chord midpoint of two ring samples is off the ring (0.74); the denoiser settles it back + on (0.029). Looping-denoise beats interpolation for staying on a curved manifold. +- NOVEL-BUT-VALID GENERATION: from-noise annealed diffusion lands on the ring (dist ~0.02, valid) BETWEEN the stored + samples (dist-to-nearest-stored ~0.04, novel) -- where bare-codebook generation just returns a stored sample + (dist-to-stored 0, degenerate). + +`holographic_diffuse` ships manifold_denoise_step (one dense-Hopfield step), settle (iterate -- denoise), and +generate (annealed diffusion -- from-noise generation). Wired as two faculties: manifold_denoise and +manifold_generate. + +Tests: +2 (981 -> 983). test_holographic_diffuse.py (the module _selftest: idempotent settling, interpolation +beaten, novel-but-valid generation, codebook degeneracy) and +test_manifold_denoise_settles_and_generate_is_novel_but_valid in test_integration.py. + +## Looping negative-lobe sharpening for arbitrary signals (shipped) + +XDATA-3, the SHARPEN half of Group G and the partner to SHARP-2 -- closing the denoise/generate/sharpen trio. A +smooth basis (low-rank reconstruction, Gaussian splat, over-consolidated truncation) LOW-PASSES a signal, +attenuating its high-frequency detail. Sharpening counteracts that by repeatedly adding a high-pass (negative-lobe) +correction -- the mechanism the splat joint-refit used (its ~51%-negative amplitudes sharpened edges), now +data-type-agnostic. + +THE HONEST SUBTLETY: the naive loop (iterated unsharp x <- x + a(x - blur(x))) DIVERGES -- its high-freq gain +(1+a)^k is unbounded, recovering detail for a few steps then exploding (measured: error 0.22 -> 0.069 at iter 6, +then -> 38 by iter 10). The stable loop is VAN CITTERT (residual-fitting deconvolution, x <- x + lam(y - blur(x))): +its accumulated operator converges to the INVERSE blur (a negative-lobe sharpening filter) with bounded eigenvalues, +so it CONVERGES. + +MEASURED on a 1-D signal (slow component + a localized high-frequency burst, Gaussian-blurred sigma=3): +- NO NOISE: looping sharpening recovers the burst and converges -- relative error 0.222 -> 0.001, no blow-up. +- WITH NOISE (kept negative): Van Cittert recovers up to an OPTIMUM then amplifies high-freq NOISE (over-sharpening). + The principled stop is Morozov's DISCREPANCY PRINCIPLE (halt when residual ||y - blur(x)|| <= noise norm): lands + near the optimum (err ~0.12 vs blurred 0.22); running UNGUARDED over-sharpens to ~0.45. +- lam above the stability bound (~2/||blur||^2) DIVERGES into ringing (err -> 1300+) -- why lam is bounded. + +`holographic_sharpen` ships _gauss_blur (default FFT low-pass) and sharpen_loop(x, blur, sigma, lam, iters, +noise_level) (Van Cittert with the discrepancy-principle guard; blur is the smoothing operator, callable). Wired as +one faculty: sharpen_loop. + +Tests: +2 (983 -> 985). test_holographic_sharpen.py (the module _selftest: no-noise recovery+convergence, noise +guard-beats-unguarded, over-large-step divergence) and +test_sharpen_loop_recovers_detail_converges_and_guard_beats_oversharpening in test_integration.py. + +This closes Group G -- the cross-data-type through-line: denoise-by-downscale (XDATA-1), looping diffusion denoise + +generate (XDATA-2), and looping sharpen (XDATA-3) are all ONE manifold operation, applicable to any data type the +engine holds, each with its honest negative and fail-safe/stability guard. + +## Smooth/sharp two-layer representation (shipped) + +CACHE-2, the architectural move borrowed from irradiance caching (cache the smooth indirect light, compute the +sharp direct light). The principle: NO SINGLE basis is cheap across a signal that is smooth in places and sharp in +others. The same split recurs in the negative-lobe sharpening finding, the SVG (smooth morph + exact vector edges), +and manifold-plus-residual decompose. At a fixed budget, split: + smooth layer = the k_smooth lowest-frequency coefficients (cheap dense basis), and + sharp layer = the k_sharp largest residual coefficients, in a basis where the sharp content is sparse. + +The earlier attempt was only a MODEST win (15.7 vs 13.7 dB) because its sharp basis was weak (pixel-exact). The win +here is LARGE because the sharp basis is the RIGHT one for the sharp content: localized spikes are BROADBAND in +frequency but SPARSE in the SAMPLE domain, so a sparse sample-domain residual holds them in a handful of +coefficients (a low-frequency basis would need a great many). + +MEASURED on a signal = two slow sinusoids + 6 spikes, at a budget covering both layers (k_smooth=6, k_sharp=6): +- SPLIT 40.4 dB vs single-FFT 28.0 vs single-sparse 18.3 -- the split wins by a wide margin. +- 30% of the signal energy sits in the residual the low-frequency layer provably cannot hold (the spikes). +- KEPT CAVEAT: at too SMALL a budget (k_smooth=4, k_sharp=4) the split LOSES (23.5 vs single-FFT 27.5) -- it cannot + afford enough of either layer; the win needs a budget large enough to hold both layers' essential coefficients. + +ANSWER to the backlog's open research question "what is the right sharp basis in the hypervector domain": whichever +one the sharp content is sparse in -- sample-sparse for spikes, a wavelet basis for edges. CACHE-2 is the STORAGE +counterpart to XDATA-3's RECOVERY: CACHE-2 stores the detail explicitly (the sharp layer); XDATA-3 recovers it from +an over-smoothed estimate. Complementary store-vs-recover. + +`holographic_twolayer` ships TwoLayerCode, smooth_sharp_split(x, k_smooth, k_sharp), smooth_sharp_reconstruct(code), +and the single-basis baselines _fft_topk / _sparse_topk. Wired as two faculties: smooth_sharp_split + +smooth_sharp_reconstruct. + +Tests: +2 (985 -> 987). test_holographic_twolayer.py (the module _selftest: split beats both single bases at +sufficient budget, sharp positions exact, small-budget caveat) and +test_smooth_sharp_split_beats_single_basis_at_fixed_budget in test_integration.py. + +## FHRR phase-domain morph (shipped) + +PHASE-1, borrowing phase-based frame interpolation's move into the PHASE domain (phase shift = motion), not +amplitude blending. Under large motion, amplitude blending GHOSTS (two faint copies fading through each other); a +phase shift MOVES the feature. FHRR is already the engine's phase domain (every atom = a vector of complex unit +phasors), so the engine gets phase-domain interpolation for free: shift each component's phase along the shortest +arc, staying on the unit-phasor manifold, instead of blending the complex vectors (the amplitude-domain morph). + +MEASURED on an FHRR fractional-power position encoding (a feature moving a large distance, xA=0.1 -> xB=0.9): +- UNIFORM MOTION (the win): the phase morph moves the decoded feature at CONSTANT velocity -- tracks the ideal + trajectory exactly (max deviation 0.000). The amplitude blend STALLS near each endpoint and rushes through the + middle (an eased S-curve, deviation 0.057), because the phase of a weighted complex sum is biased toward the + heavier endpoint. +- ENERGY / VALIDITY: the phase morph is a valid unit phasor at every t (|z_j|=1). The amplitude blend COLLAPSES + where components fall out of phase -- mean magnitude 0.75 at the midpoint (toward 0.64 for independent states), + so it is not even a valid FHRR vector without renormalising. + +THE HONEST NEGATIVE (kept loud): phase-domain morphing is NOT a free win under arbitrarily large change. The morph +uses the SHORTEST ARC per component, which WRAPS once a component's phase difference exceeds pi -- past that it +takes the wrong way round and stops tracking the true intermediate (measured: at a separation where phase diffs +reach ~1.6*pi, deviation 0.983 -- completely lost). And near-orthogonal endpoints have no well-defined intermediate +for ANY method. So the win holds while the change keeps per-component phase differences under pi; beyond that it +degrades gracefully on energy (still unit phasors) but not on tracking. + +`holographic_phasemorph` ships phase_morph(a,b,t) (shortest-arc phase interpolation) and amplitude_morph(a,b,t) (the +baseline blend). Wired as one faculty: phase_morph. This connects to the WiFi/CSI phase-as-information thread on +record: phase IS the information, and interpolating it directly is what FHRR's phasor domain makes natural. + +Tests: +2 (987 -> 989). test_holographic_phasemorph.py (the module _selftest: uniform-motion win, energy +preservation, wrapping negative) and test_phase_morph_uniform_motion_and_energy_with_wrapping_negative in +test_integration.py. + +## Adaptive iteration count for the resonator (shipped) + +ADAPT-2, the variance-gate applied to iteration COUNT rather than sample count. The SBC resonator +(holographic_sbc.sbc_resonator / decompose_structure) factors a bound product by annealed alternating projection. It +already returned early per RESTART once the picks verified, but its INNER loop always ran a fixed 50 iters even after +the estimate had converged. Adaptive sampling is the engine's own pattern (the SPRT in the recall path), and the +resonator has an even cleaner stop signal: an EXACT reconstruction. + +The opt-in `early_stop=True` (default off, bit-identical when off) stops the moment the picks RECONSTRUCT the product +exactly. Because no further iteration can improve a verified answer, this returns the SAME verified answer the fixed +count would, only sooner -- so it is RISK-FREE (accuracy never changes, iters never increase). + +MEASURED (B=24, L=7, F=3): +- EASILY-SOLVABLE workload (codebook N=10): early-stop cut average iters ~62% (68 -> 26) at IDENTICAL accuracy + (19/20 == 19/20). A single solvable problem went 50 -> 6 iters, same verified picks. +- HARD / mostly-unsolved workload (N=50): 0% change, 0 harm -- unsolved problems never verify, so they run the full + search either way. The win is workload-dependent: large where the fixed count over-computes an easy problem, a + clean no-op where the search genuinely needs the iterations. + +Wired additively: `early_stop=False, min_iters=5, stats=None` on sbc_resonator and decompose_structure (module), and +`early_stop=False, stats=None` on the UnifiedMind decompose_structure faculty. Pass stats={} to read stats['iters'] +(the inner iterations actually run) so the saving is measurable. Existing SBC suite passes unchanged (back-compat). + +Tests: +2 (989 -> 991). test_holographic_adaptive_resonator.py (the module _adapt2_selftest: matched accuracy at +lower avg iters on solvable, no-op on hard) and test_decompose_structure_early_stop_matches_at_lower_cost in +test_integration.py. + +## Adaptive curvature-driven cache anchor placement (shipped) + +CACHE-3, irradiance caching's adaptive record density instead of a uniform grid. Uniform placement wastes anchors on +flat regions and under-resolves the bends; the GI literature reports ~7x fewer records for the same quality with +adaptive density. The same waste applies to any cache or codebook over a field with non-uniform smoothness. + +THE RULE (equidistribution). For piecewise-linear reconstruction the error on an interval of width h scales like +|f''|*h^2, so to make every interval contribute equally: |f''|*h^2 = const -> h ~ |f''|^(-1/2) -> anchor DENSITY ~ +|f''|^(1/2). Estimate the curvature, raise to the 1/2 power, add a small floor so flat regions still get a few +anchors, place anchors at equal-mass quantiles of that density (inverse-CDF sample). + +MEASURED on a gentle slope + one sharp narrow bump: +- adaptive placement matches uniform quality at ~7.5x FEWER anchors (uniform needs 239 to match adaptive-32), and at + a fixed count is far better (N=32: uniform RMSE 0.070 vs adaptive 0.0017) -- the bump resolved, not stepped over. +- HONEST CONTROL (kept scope): on a UNIFORMLY-smooth field (a plain sinusoid) adaptive does NOT beat uniform + (uniform-32 0.0106 vs adaptive-32 0.0084, ~tied) -- no curvature concentration to exploit. The win is quality + MOVED to where the field needs it, not free quality; it is specifically a property of NON-uniform smoothness. + +Ties to ADAPT-1 (residual-peak splat placement, gradient-ish) and CACHE-1 (the irradiance cache whose anchors this +places), and to the Group H AO-1 local-crowding hypothesis. `holographic_adaptive_cache` ships adaptive_anchors(x, +y, n, floor, power) and reconstruct_from_anchors(x, anchor_x, y). Wired as two faculties. + +Tests: +2 (991 -> 993). test_holographic_adaptive_cache.py (the module _selftest: adaptive beats uniform at fixed N +and at ~7x fewer anchors, ~tied on a smooth field) and test_adaptive_anchors_beat_uniform_on_nonuniform_field in +test_integration.py. + +## Backward warping is hole-free (shipped / validated) + +PHASE-2, a validated note (not a new faculty -- unbind already is the backward map). Frame interpolation moved from +FORWARD warping (push each source pixel to where it goes) to BACKWARD warping (for each target pixel, pull from where +it came) because a forward warp under a non-uniform deformation leaves HOLES (target cells no source landed on) and +OVERLAPS (cells several sources collide on), while a backward warp visits every target exactly once and fills them +all by construction. The engine gets the backward form for free: unbind is a BACKWARD, invertible map -- to recover a +stored value you take the target role and unbind its source out (a gather), not scatter the composite forward and +hope every slot fills. + +MEASURED (a signal resampled under a non-uniform but monotonic warp warp(s) = s + 0.12*sin(2pi*s), N=256): +- FORWARD scatter: 62 holes + 39 overlaps out of 256 cells (the warp locally stretches -> gaps; locally compresses + -> collisions). +- BACKWARD gather: 0 holes, reconstruction RMSE 0.0 -- every target read its source exactly. + +The note for the engine: wherever it could either splat a representation forward or unbind it backward, the backward +route is the hole-free one to prefer. `holographic_backwardwarp` ships forward_scatter (the cautionary baseline) and +backward_gather (the unbind form) as the demonstration behind the note; no UnifiedMind faculty (unbind already +exists). + +Tests: +1 (993 -> 994). test_holographic_backwardwarp.py (the module _selftest: forward leaves holes+overlaps, +backward leaves none and is exact). + +## Multi-resolution pyramid / mipmap (shipped) + +SCALE-1, making coarse-to-fine an explicit ARCHIVE (mipmaps / flow pyramids / 3DGS densification). Keep the signal at +several resolutions, read the level a query needs, refine toward fine only where it matters. The engine already leans +this way implicitly (recursive/fractal structure, HoloForest's coarse descent, consolidation's low-rank-first); this +makes the multi-resolution archive explicit. + +THE DECISIVE PROPERTY: anti-aliasing on a COARSE read. You cannot get a low-resolution view by SUBSAMPLING the full +store -- content above the coarse Nyquist FOLDS into the low band and corrupts it (aliasing). A mipmap level was +LOW-PASS FILTERED before downsampling, so its coarse view is clean. Each level is also smaller (cheap coarse read), +and the levels are a progressive code (coarsest is a usable approximation, finer levels add detail back, exact at top). + +MEASURED (a low-freq signal + a high freq ABOVE the coarse Nyquist; pyramid [1024, 512, 256, 128, 64]): +- a 1/8 coarse query matches the true low-frequency band ~11x better than a naive subsample (mipmap RMSE 0.035 vs + naive 0.388), which aliases the high frequency into a spurious low tone (the aliased bin has >100x the spurious + energy under naive vs mipmap). +- each level is half the size of the one below (cheap LOD read); the full level reconstructs exactly. + +RELATION TO CACHE-2 (kept honest): CACHE-2's smooth/sharp split is a fixed TWO-level decomposition tuned to a storage +budget; SCALE-1 is the multi-LEVEL spatial hierarchy with the distinct anti-aliased-LOD property (each coarse level +is a smaller, alias-free array readable on its own). Same family, different job. Relates to XDATA-1 (downscale = +low-pass = the same anti-aliasing, here stacked into a pyramid). `holographic_multires` ships build_pyramid, +upsample_to, naive_subsample (the baseline). Wired as two faculties: multires_pyramid + pyramid_reconstruct. + +Tests: +2 (994 -> 996). test_holographic_multires.py (the module _selftest: anti-aliased coarse query beats naive +subsample, levels halve, full level exact) and test_multires_pyramid_anti_aliased_coarse_query in test_integration.py. + +## Re-anchoring is load-bearing for deep traversal (shipped / audited) + +RAY-2, the path-traced form of "a shared kernel is not a shared manifold." In the FFT/phasor domain a bind is +elementwise complex multiplication, so a chain of binds is a ray whose recoverable signal ATTENUATES multiplicatively +with each hop. The fix is next-event estimation: connect to a KNOWN anchor (the codebook) at every bounce via cleanup +-- re-project the intermediate state onto the manifold each step. Without it the state drifts off-manifold and the +signal collapses. + +THE AUDIT (the VALIDATE half): every deep-composition / traversal faculty already re-anchors at each step -- +gated_traverse (RAY-1) and directed_traverse (RAY-3) clean up inside their step; the peel-based decode_structure +cleans up per peel (measured 2 -> 15 hops); the pack/recover and nested-decode paths resolve each item to the +codebook. No deep path is missing the discipline, so there is no cleanup to add -- RAY-2 is a validation, not a build. + +THE CONTRAST (what the existing tests omit): the traverse self-test shows the RE-ANCHORED traversal works, but never +shows it FAILING without re-anchoring -- the whole claim. This drives the engine's REAL gated_traverse over a directed +linked list two ways, identical except for the one line that carries the CLEANED node forward vs the RAW one. + +MEASURED (a 12-hop directed linked list in superposition): +- RE-ANCHORED reaches every hop (12/12) in order, then the throughput gate abstains exactly when the chain runs out + (the signal is genuinely gone, not lost to drift). +- RAW collapses almost immediately (~1 hop): the carried noise compounds each hop, throughput falls through the + floor, and the gate stops the dark ray. Per-hop cost: one codebook argmax (O(vocab)) -- cheap, and plainly + justified, since without it the traversal does not survive past the first hop. + +Complements peel's BUNDLE result (iterated decode 2 -> 15) with the CHAIN case, on the real faculty. +`holographic_reanchor` ships directed_linked_list (build the superposed chain) and make_steps (the re-anchored vs raw +step functions); no new faculty (gated_traverse already is the faculty -- this audits it). + +Tests: +2 (996 -> 998). test_holographic_reanchor.py (the module _selftest: re-anchored reaches all hops, raw +collapses early) and test_reanchoring_is_load_bearing_for_deep_traversal in test_integration.py. + +## Jittered sub-pixel splat accumulation -- KEPT NEGATIVE (ACCUM-1) + +ACCUM-1, the "TAA/DLSS done correctly" idea. The splat fit places every splat at an INTEGER grid position (residual +peak) and the joint refit keeps positions fixed, so the natural idea: jitter the FIT at sub-pixel offsets across +passes (Halton/golden-ratio) and accumulate, letting splats land between grid points to sharpen sub-pixel edges. The +honest question: does it sharpen PAST the joint refit? MEASURED ANSWER: NO. + +MEASURED (a continuous target with a sharp SUB-PIXEL feature, K splats, scored at high resolution): +- REFIT-ONLY (base grid): RMSE 0.0201 -- grid-aligned splats can't sit on the sub-pixel feature. +- JITTERED accumulation (fit K/j on j sub-pixel-shifted grids, accumulate, joint-refit): RMSE 0.0022 -- better than + base, BUT only because it SAMPLES the target at sub-pixel offsets (supersampling), not because of jittering. +- THE CONTROL THAT SETTLES IT: given the SAME sub-pixel samples, fitting DIRECTLY on a 4x-finer grid (an ordinary + refit at higher resolution) is RMSE 0.0011 -- STRICTLY BETTER than the jittered accumulation. A global greedy + + joint refit over all sub-pixel positions beats fitting each shifted grid independently and summing. +- AND with NO new info (shifted grids interpolated from the base grid), jittering can't manufacture sub-pixel detail + the base samples never held. + +THE NEGATIVE: jittered sub-pixel accumulation is NOT a sharpening tool. The only lever is the SAMPLING RESOLUTION of +the target -- if you have sub-pixel samples, fit directly on them (a finer-grid refit wins); if you don't, jittering +adds nothing. Pixel-aligned placement + joint refit, at sufficient sampling resolution, is already the right answer. +Consistent with the earlier no-op (supersampling a band-limited Gaussian sum has nothing to anti-alias). Nothing is +wired -- `holographic_jittersplat` records the experiment and the negative. + +Tests: +1 (998 -> 999). test_holographic_jittersplat.py (the module _selftest: jittered beats base only by +supersampling; a finer-grid refit beats jittered -- jittering doesn't sharpen past the refit). + +## Anisotropic splat-fit adaptive stop -- C3 (cross-cutting: ADAPT-2 -> image gen) + +The first cross-cutting transfer: the resonator's adaptive-stop (ADAPT-2) applied to splat_aniso's gradient +fit, which optimises the covariances for a FIXED 200 Adam steps. Stop when the reconstruction MSE has converged. + +THE CRITERION (and why the obvious one fails twice): +- Relative-improvement-over-a-window vs the CURRENT MSE FAILS on a near-perfectly-fittable field: the fit + descends geometrically toward zero, so each window still halves the error (> tol relative) forever and the + stop never fires. FIX: measure window improvement against the INITIAL error (a fixed scale) -- it fires + whether the fit plateaus at a residual floor OR descends geometrically toward zero. +- Adam's momentum needs ~30 steps to warm up; during the warm-up the MSE barely moves, so a naive test + mistakes it for convergence and stops at step ~20 with a terrible fit. FIX: a min_steps floor (default 40). + +MEASURED: ~20-40% fewer steps on under-fit fields (a busy 9-blob field stops near ~121 of 200), less on a +near-perfectly fittable one, at a few-percent MSE cost. + +THE KEPT CAVEAT (the honest difference from ADAPT-2): the resonator early-stop is FREE because it has an EXACT +reconstruction certificate (stop when the picks verify -- same answer, sooner). A continuous gradient fit has +only a SOFT plateau, so stopping ALWAYS costs a little MSE -- this is a speed/quality KNOB, not a free lunch. +Off by default (early_stop=False is bit-identical to the fixed-step fit). + +Wired as early_stop= on aniso_fit and the splat_aniso faculty (pass stats={} to read stats['steps']). + +Tests: +2 (999 -> 1001). test_holographic_aniso_earlystop.py (the module _c3_selftest) and +test_aniso_early_stop_saves_steps_at_small_cost in test_integration.py. + +## Adaptive-stop diffusion -- B3 (cross-cutting: ADAPT-2 -> text gen) + +The resonator's adaptive-stop applied to generate_structure (the B10 composed-manifold diffusion), which runs +a FIXED annealing schedule. The structure being built -- read as the hard combination of fillers per slot +(_decode_combo: unbind each role, argmax the filler) -- SETTLES well before the schedule ends, so stop once it +has been stable for `patience` steps past a `min_steps` floor (default steps//2, past the high-noise phase). + +ENO'S CONDITION (don't amputate novelty): stop on STABILITY, not first-convergence. The late, lower-noise part +of the walk is where different seeds diverge into different structures; cutting it off on the first converged +step would collapse diversity. Stability-for-`patience`-steps past a floor preserves it -- MEASURED: 20 distinct +structures both ways, and the SAME structure as the full run on every seed (so the stop changes WHEN it lands, +not WHERE). + +WHY IT IS FREE (unlike C3): a continuous splat fit has only a soft plateau, so stopping always costs a little +MSE. Here the hard decoded combination is an effective CERTIFICATE -- once it is stable, the output is +determined. The early-stopped z is mid-anneal (slightly less sharp: validity ~0.967), so on stopping we apply +one final crisp `_structure_project` at full beta with NO noise, which sharpens the settled combination and +restores validity to 1.000. Same structure, full validity, ~50% fewer steps. + +Wired as early_stop=/min_steps= on generate_structure (module + faculty; pass stats={} to read stats['steps']). +Off by default (early_stop=False is bit-identical to the fixed schedule). + +Tests: +2 (1001 -> 1003). test_holographic_diffusion_earlystop.py (the module _b3_selftest) and +test_generate_structure_early_stop_matches_full_at_half_the_steps in test_integration.py. + +## Splat-render sharpening -- C4 (cross-cutting: XDATA-3 -> image gen) -- KEPT NEGATIVE + +THE PROPOSAL (Milanfar's seat, RED/Van Cittert): a splat render is a sum of smooth Gaussians, hence +over-smoothed (splat_aniso's own negative says a few Gaussians cannot hold high frequency), so sharpen it with +the XDATA-3 negative-lobe loop to recover edge detail. High upside on paper. + +THE MEASURED ANSWER: it does NOT work, for a STRUCTURAL reason (not tuning). Van Cittert deconvolution assumes +the smooth signal is blur(truth) -- a CONVOLUTION of what you want. A splat render is not that: it is a sparse +sum of Gaussians, ~= blur(the splat CENTRES), a handful of spikes. Deconvolving it drives toward those centres +(spikes/ringing), NOT toward the discarded edges. Sharpening the render at every sigma/iters tested makes it +WORSE (relative error rises ~5-8%). + +THE DECISIVE CONTROL: the SAME 2-D Van Cittert sharpener on a GENUINE Gaussian blur of the truth RECOVERS ~42% +of the error (0.37 -> 0.22). The machinery works; the negative is specifically that a splat render is +sum-of-Gaussians(centres), not blur(truth). + +THE LESSON: the image-domain twin of the ACCUM-1 jitter negative and the generate_vector bare-codebook negative +-- you cannot manufacture detail that was never stored. A lossy smooth basis THREW AWAY the high frequency; no +negative-lobe loop recovers information that is not in the render. Sharpening un-low-passes a genuinely +low-passed signal; it cannot un-throw-away a lossy approximation. + +No faculty, no tour line (the finding is the negative). The 2-D Van Cittert (gauss_blur2 + vc_sharpen2 in +holographic_splatsharpen.py) is the vehicle for the control. + +Tests: +1 (1003 -> 1004). test_holographic_splatsharpen.py (the module _selftest: control recovers from a true +blur, negative shows the splat render cannot be improved at any setting). + +## Robust reward/value accumulation -- D2 (cross-cutting: ACCUM-3 -> creature brain) + +ACCUM-3's outlier clamping applied to the creature brain's value memory. Each prototype keeps a running-mean +return (`_ret[a][j] += alpha*(ret - mean)`); a single freak reward (a jackpot, a sensor glitch) folds straight +in and drags the estimate. robust_returns winsorises the residual to +/- k * `_ret_dev` before it lands, where +`_ret_dev` is the running typical |residual| (the reward NOISE scale). + +THE DESIGN CHOICE (why ONE global scalar, not a per-prototype array): `_ret` is touched at 8+ sites (init, +append, evict, reorganize, clone, save/load); a parallel per-prototype scale array would be invasive and would +break old saves. ONE running scalar suffices because the noise SCALE -- unlike the mean -- is roughly constant +across prototypes, so a global |residual| estimate winsorises a mean-1 prototype and a mean-5 prototype equally +well (measured). It is cheap, serialises trivially (it is transient scratch -- like the existing EMAs, it is NOT +persisted and re-seeds from the first post-reload residual; only the FLAG is saved, via _STATE_FIELDS). + +MEASURED: under 8% outlier rewards, ~3x lower value error than the plain running average (1.57 -> 0.53 in +isolation; the integration/selftest assert the brain's value() is markedly closer to the true mean). On CLEAN +data: no cost (0.0561 vs 0.0550). The win is ~3x, not ACCUM-3's ~100x, because the floored-alpha EMA already +damps outliers somewhat -- winsorisation adds the rest. + +Off by default (robust_returns=False -> the plain update path is bit-identical). Wired as robust_returns= on +HolographicMind and the actions() faculty; carried through _blank/_clone and persisted via _STATE_FIELDS. + +Tests: +2 (1004 -> 1006). test_holographic_robust_returns.py (the module _d2_selftest: lower error under +outliers, no clean-data cost, flag survives save/load) and test_robust_returns_resists_outlier_rewards in +test_integration.py. + +## Coarse-to-fine splat densification -- C1 (cross-cutting: SCALE-1 + ADAPT-1 + 3DGS -> image gen) + +3D-Gaussian-Splatting densification, from scratch. The one-shot aniso_fit places all K splats by matching +pursuit then runs ONE joint gradient fit; its kept negative is that the non-convex loss makes the result +depend on the warm start (a poor local optimum, sometimes divergence). densify_fit grows the set in STAGES: +place a fraction on the current residual (coarse scales first), jointly optimise ALL, place more where the +re-optimised fit still errs, optimise again. + +THE MEASURED WIN (and why it is real, not just more steps): on a multi-scale target (broad blob + small sharp +details) densify reaches MSE ~1e-6 where the one-shot plateaus near ~1e-3 -- and the one-shot CANNOT close the +gap at ANY step count (measured 280/450/700 steps: it stays ~1e-3 and then DIVERGES past ~300, the non-convex +instability the negative warns of). So the staged placement is a strictly better WARM START, landing the final +joint fit in a basin the one-shot never finds. At MATCHED total compute (splat-steps) densify already wins; the +trade is that it uses several optimisation rounds, and the win is specific to MULTI-SCALE content (on a +single-scale field the one-shot is already near-optimal). + +NOT manufacturing detail (contrast C4): C4 tried to sharpen detail the splats discarded and failed (you cannot +recover what was not stored). C1 does the opposite -- it finds a better ARRANGEMENT of the detail genuinely +present in the target. Different operation, different (positive) result. + +REFACTOR: the Adam loop was extracted into the shared `_aniso_optimize(target, centers, amps, Ls, ...)` so +aniso_fit (iso warm start) and densify_fit (staged warm start) use ONE gradient engine -- no duplication; the +C3 early-stop lives in the helper. aniso_fit is bit-identical after the refactor (its selftest + the splat +suite confirm). Wired as the `splat_densify` faculty (pass stats={} to read stats['stages']). + +Tests: +2 (1006 -> 1008). test_holographic_densify.py (the module _c1_selftest: densify reaches a markedly +better optimum than the one-shot) and test_splat_densify_beats_one_shot_on_multiscale in test_integration.py. + +## Adaptive encoder resolution -- A3 (cross-cutting: CACHE-3 -> encoder) -- the one promising below-stack item + +CACHE-3's equidistribution (place resolution by density) applied to the ScalarEncoder. The sweep's premise was +that the kernel is already near-optimal, so below-stack transfers are mostly negative -- A3 is the exception. + +THE MAPPING: the ScalarEncoder is NOT a grid of kernels -- it is a Fourier-phase encoder whose kernel is +shift-invariant (uniform resolution across [lo,hi] by construction, Bochner). So "place kernels adaptively" has +no discrete kernels to move; the equivalent is to WARP the input axis by the value-density CDF, stretching dense +regions so they get finer effective resolution. fit_resolution(samples) fits that monotonic warp; encode warps +x before the phase rotation, decode unwarps the result. + +THE FLOOR (the irradiance-caching validity-radius lesson, AGAIN): a PURE CDF warp drives sparse regions to +~zero resolution, where decodes go catastrophic (measured: sparse-region error 0.0073 uniform -> 0.2569 warped, +~35x WORSE) -- and those catastrophic tail decodes drag down the AVERAGE too. Mixing the CDF with the identity +(floor=0.2: keep >= 20% resolution everywhere) bounds the sparse loss to ~4x and LIFTS the in-distribution win +from ~15% to ~73%. Local weights with a validity radius, third appearance (after irradiance caching and the +splat refit). + +MEASURED: non-uniform (bimodal) distribution ~55-73% lower decode error under noise; UNIFORM distribution ties +(warp = identity -- the control proving the gain is from density structure, not the machinery). KEPT CAVEAT: a +REALLOCATION, not free -- dense decodes ~4x better, sparse/out-of-distribution ~4x worse (floor-bounded). Fit +only when decoding in-distribution values. + +REFACTOR: encode() split into _phase_encode(u) (the raw Fourier encoding) + the warp; decode() builds its grid +with _phase_encode and unwarps the result. Unfitted (no fit_resolution call) -> warp is the identity -> +bit-identical to the plain encoder. A primitive enhancement (no UnifiedMind faculty -- the mind has no scalar +faculty to attach it to; a faculty must earn its method). + +Tests: +2 (1008 -> 1010). test_holographic_adaptive_encoder.py (the module _a3_selftest: win on non-uniform, +tie on uniform, unfitted bit-identical) and test_adaptive_encoder_resolution_on_nonuniform_data in +test_integration.py. + +## Low-discrepancy exploration -- D1 (cross-cutting: SAMPLE-1 -> creature) -- KEPT NEGATIVE + +THE PROPOSAL (Togelius's seat, caveat on record): SAMPLE-1's low-discrepancy sampling covers a space more evenly +than i.i.d. random, so drive the creature's exploration from a low-discrepancy sequence instead of epsilon-random. + +THE MEASURED ANSWER: it actively HURTS (not just neutral), for a structural reason. SAMPLE-1's win is for placing +each sample INDEPENDENTLY. A creature WALKS its state space, and a walk ACCUMULATES displacement. A +low-discrepancy sequence over the four moves is BALANCED (N vs S, E vs W spread evenly in time), so the steps +CANCEL and the agent stays pinned near start. A random walk's runs and imbalances ARE the diffusive drift that +explores. Measured (open grid, 400 steps): random ~162 distinct cells, low-discrepancy ~12 -- an order of +magnitude WORSE. + +THE LESSON: this pins down WHY a transfer that pays for direct sampling fails for sequential exploration. Low +discrepancy MINIMISES the imbalance of a point set; spatial exploration NEEDS the imbalance (displacement is the +cumulative SUM of the steps; a balanced sum is ~zero). Opposed goals. Togelius's caveat ("buys almost nothing +over a handful of discrete actions") is stronger than predicted -- harmful, not neutral. The real coverage lever +is count-based / novelty exploration, partly already in the brain's novelty_bonus. + +No faculty, no tour line (the finding is the negative). + +Tests: +1 (1010 -> 1011). test_holographic_ldexplore.py (the module _selftest: low-discrepancy covers far fewer +cells than random; sanity that random does drift). + +## MIS-weighted steered generation -- B1 (cross-cutting: MIS-1 -> text gen) -- KEPT NO-OP + +THE PROPOSAL (Pharr's seat, balance heuristic, precondition on record): steered_generate keeps the candidate +with the best verifier (coherence) score among the predictor's top-beam, discarding the predictor's ranking at +selection. So combine the predictor's coupling score and the verifier's coherence score by the balance +heuristic, weighting each by reliability, instead of letting the verifier override. + +THE MEASURED ANSWER: NO-OP -- the balance combination gives results IDENTICAL to verifier-only, structurally. +steered_generate already uses the predictor as the candidate GATE (it restricts to the top-`beam` before the +verifier picks). WITHIN that beam the coupling scores are nearly flat (all are the most-probable continuations), +so after the softmax that puts them on a common scale the predictor's factor is ~uniform and cannot move the +argmax of the product. The verifier dominates -> MIS == verifier. The predictor's information is ALREADY fully +spent on gating; re-using it as a within-beam weight is redundant. + +MEASURED (loop-trap corpus: a frequent 'ping pong' cycle mixed with coherent clauses): the verifier DOES escape +the greedy loop (distinct-token ratio ~0.44 vs greedy ~0.15 -- the setup is real), but the balance combination +matches the verifier EXACTLY on both fluency (valid-bigram rate) and anti-looping (distinct ratio). + +THE LESSON: MIS combines two estimators of the SAME quantity over a COMMON support on a common scale (Pharr's +precondition). The predictor does not estimate over the same set as the verifier -- it FILTERS to its top-beam +first -- so there is nothing for the balance heuristic to balance. A gate followed by a re-ranker is not the MIS +setting. (Compare D1: another transfer that fails because the operation is structurally different from the one +the technique was built for.) + +No faculty, no tour line (the finding is the no-op). + +Tests: +1 (1011 -> 1012). test_holographic_misgen.py (the module _selftest: verifier escapes the loop -- setup +real; MIS == verifier on distinct ratio -- the no-op). + +## Phase-domain image morph -- C2 (cross-cutting: phase vocoder / PHASE-1 -> the image morph path) + +The SAME phase-domain lesson PHASE-1 already established for FHRR vectors, now applied to morph_scene (which only +did DCT-coefficient slerp). NOT a new principle -- the rediscovery is honest: PHASE-1 + its wrapping bound were +already on record for vectors; C2 is its application to images. + +THE MAPPING: morph in the 2-D FFT domain, interpolating each bin's MAGNITUDE linearly and PHASE along the +shortest arc. By the Fourier shift theorem a translation is a phase ramp, so phase interpolation SLIDES a +translated feature to its intermediate position (a compact moving blob) where the DCT slerp interpolates the +feature's SHAPE and SMEARS it into an elongated oval. VERIFIED VISUALLY (rendered the frames, did not trust the +scalar): at shift 6 the DCT midpoint is a stretched oval, the phase midpoint a compact round blob sliding cleanly. + +MEASURED (blob on 48x48, metric = midpoint peak / endpoint peak): shift 6 -> DCT 0.85 vs phase 0.97 (phase +WINS); shift 16 -> DCT 0.70 vs phase 0.67 (the wrap -- phase slightly WORSE). Through morph_scene on a 28x28 +field: phase 0.83 vs dct 0.73. + +THE BOUND (kept loud, same as the vector version): phase is mod 2*pi. For a LARGE translation the bin phase +differences exceed pi, the shortest arc wraps, and the morph falls back to a ghosted crossfade (rendered: BOTH +methods show two blobs at shift 24). The win holds only within the per-step displacement that keeps bin phase +differences under pi -- the Nyquist limit on phase, exactly the phase-unwrapping problem the vocoder lives with. +So method='phase' is for small-motion morphs, method='dct' for arbitrary structure change. + +Wired as morph_scene(method='phase') (default 'dct' -> bit-identical). Added morph_image_phase to the existing +holographic_phasemorph.py rather than a new module (same family). + +Tests: +2 (1012 -> 1014). test_holographic_phasemorph_image_c2_selftest (the module _c2_selftest: phase slide +beats crossfade small, wraps large) and test_morph_scene_phase_slides_translation_better_than_dct in +test_integration.py. + +## Re-anchored lookahead for the creature -- D4 (cross-cutting: RAY-1 -> model-based planning) -- KEPT NEGATIVE + +THE PROPOSAL (the research item; Baker / Adamatzky): the creature is purely REACTIVE (best learned value in the +current state, no forward model). Give it one -- a per-action transition operator learned from its experience -- +roll it out a few steps to imagine each action's consequences, pick the best rollout, RE-ANCHORING each predicted +state (RAY-1: clean up every hop or the rollout compounds error and decays). + +PART 1 -- THE MECHANISM WORKS (RAY-1 confirmed in a new domain). Forward model: delta_a = normalize(mean +unbind(next, state)); predict(s,a) = bind(s, delta_a). Naive rollout DECAYS with depth (cosine to true 0.65 -> +0.53 over four steps). Re-anchoring the predicted state to a codebook of seen states each step holds it +ON-MANIFOLD (cosine ~constant 0.77 across depth). Re-anchoring is as load-bearing here as for bind-chain traversal. + +PART 2 -- THE APPLICATION IS REDUNDANT (the kept negative). Re-anchored lookahead ranks the four actions +IDENTICALLY to the plain reactive value -- 98-100% action-rank agreement -- so it can never decide differently; +on stars it ties the reactive policy and loses its small epsilon (marginally worse). PRECISE ROOT CAUSE +(diagnosed): the four predicted leaves sit at 0.974-0.994 PAIRWISE COSINE -- a single per-action bind displacement +COLLAPSES all actions to nearly the same predicted next-state, because in the egocentric sense-space the AVERAGE +sense-change is similar across directions (directional specificity is lost in the averaging). So the lookahead +bonus varies by std 0.0075 across actions while the reactive value varies by std 0.37 -- lookahead carries no +differentiated signal. Secondary structural reason: the creature's value IS the Monte-Carlo discounted return, +already horizon-aware, so model-based planning recomputes (through a noisy model) what the model-free value +already encodes. + +THE CROSS-CUTTING LESSON (throughline with C4, D1, B1): the re-anchoring transfer is mechanically sound, but the +creature's state space does not admit a forward model good enough for the application. The structural mismatch is +egocentric-sense-space averaging -- a per-action linear/bind operator cannot capture the position-dependent, +action-specific consequences a real lookahead needs, so it predicts the same future for every action and the +planner is blind. A right technique applied to an operation whose shape defeats it -- the same failure as the +splat sharpener (a sum is not a blur), LD exploration (independent points for a sequential walk), and MIS +generation (a gate, not a second estimator). + +No faculty, no tour line (the finding is the negative). + +Tests: +1 (1014 -> 1015). test_holographic_lookahead.py (the module _selftest, CI-fast ~1.7s: the forward model +collapses the actions -- leaves > 0.9 pairwise cosine -- so lookahead-vs-reactive rank agreement > 0.9). + +## The cross-cutting PROBE SWEEP -- A1, A2, B2, B4, D3, D5 -- SIX KEPT NEGATIVES + +The cross-cutting backlog's probe items: transfers the panel pre-judged as no-ops. All six measured on the real +substrate, all six confirmed the prior. They ship as ONE shared module (holographic_probesweep.py) with one +measurement+assert and one test each -- no faculty, no tour line. The reason each fails is the artifact, and it is +the C4/D1/B1/D4 throughline: a sound technique applied to an operation whose shape defeats it. Two failure classes: + +CONCENTRATION OF MEASURE (the kernel is already near-optimal -- no slack to win): + A1 LD/blue-noise codebook [SAMPLE-1 -> kernel]. Riesz repulsion vs i.i.d. atoms: max coherence ~3% lower, MEAN + coherence unchanged, at every dim 64..1024; capacity identical (40 pairs in d=512: 0.70 recall both); 500 + steps barely move it. Random atoms are already near-uniform on the sphere. NO-OP. + B4 LD sampling in generation [SAMPLE-1 -> text]. LD-noise diffusion == i.i.d.-noise diffusion on diversity (41 vs + 42 distinct atoms) and validity (0.507 vs 0.510) -- the diffusion is ATTRACTOR-dominated (cleanup decides the + landing, not the noise). A categorical token draw is a single pick and cannot use LD at all. NO-OP. + D5 Observation denoising [XDATA-1/2 -> creature]. Snapping the noisy state to the seen-state manifold does not + improve the decision -- argmax preserved ~equally from raw and denoised (0.68 vs 0.68 low noise) because the + value's similarity-weighting already absorbs noise -- and OVER-SMOOTHS at low noise (value err 0.34 -> 0.42). + The high-dim encoder is its own denoiser. NO-OP. + +WRONG-SHAPED OPERATION (the technique's precondition does not hold): + A2 Negative-lobe cleanup sharpening [XDATA-3 -> kernel]. Deconvolving the similarity profile (subtract + alpha*(G-I)@s) then argmax HURTS discrete cleanup (correlated atoms: 0.18 -> 0.00, amplifies noise) and is a + no-op for orthogonal atoms (G~=I). Hard NN is already Bayes-optimal for 'which atom'. NEGATIVE. + B2 Throughput-gated generation [RAY-1 -> text]. The running coherence does NOT separate in-distribution from a + garbage seed (means -37 vs -39, overlapping) -- steered generation pulls any start back to coherent + continuations, so there is no incoherent tail for a gate to catch. The coherence defense is redundant; the + abstention has nothing to fire on. REDUNDANT NO-OP. + D3 MIS-combined decision [MIS-1 -> creature]. In typical states the soft blend == the veto (0 lethal choices over + 84 sensed-danger states, because value already disfavours danger). But the veto's value is the RESIDUAL the + value misestimates (survival bench: ~0.6%/step -> 67-73% of long lives die without it); a soft penalty picks a + lethal move whenever the value margin exceeds the penalty, so it cannot give the guarantee. A safety + constraint is not an estimator to blend. NEGATIVE. + +Tests: +6 (1015 -> 1021). test_holographic_probesweep.py (one test per probe; the module _selftest runs all six +asserts, CI-fast ~0.8s). + +## Creature-brain performance pass: value_batch / budget knob -- and a MEASURED NEGATIVE on batched value + +Three requests for holographic_creature, all bound by the hard constraint that the creature is +TIE-SENSITIVE (a 1e-16 difference at the top-k boundary flips a maze trajectory -- the bind_batch +lesson). So every change had to be BIT-IDENTICAL to enter the decision path, or stay out. Each was +measured before shipping. + +REQUEST 1 -- batched value() for speed: MEASURED NO SPEEDUP, kept negative. The premise was that a +single stacked `U_all @ state` plus a segment-reduce would cut the hot path. It does not, for two +reasons measured on real trained brains (banks ~84/action, dim 512, 6000 value calls): + - No speedup at any scale. A bit-identical per-action-gemv batch is ~2% faster (the value() + wrapper overhead is negligible); a stacked one-matmul is 73% SLOWER (the per-call concatenate + costs more than it saves); a PERSISTENT stack (no per-call concat) is 0.94-1.01x at banks + 84/300/800; a padded-tensor vectorised top-k is 1.03-1.05x. BLAS already runs four small + matrix-vector products about as fast as one big one, and the per-action top-k (argpartition) + is irreducible -- it cannot be merged because k is per action. + - The stacked matmul is also NOT bit-identical to the per-action product: BLAS gemv blocks by row + count, so `concat(U) @ s` differs from `[U_a @ s]` at ~1e-16 -- the exact tie-break hazard that + kept bind_batch out of the encoder. (An M-INDEPENDENT reduction `(U*s).sum(1)` IS batchable + bit-identically, but does not match gemv, so adopting it would shift the whole baseline.) + A cProfile of a real run shows why the lever was misjudged: decide() is 44% of the run and + encode() (the per-sense binds -> FFTs; _raw_fft alone is 25% of total) is the OTHER 44%. value() + is genuinely efficient; the cost is real work, not Python overhead. The other half (encode) is the + bind_batch territory already known to be tie-unsafe. value_batch is still SHIPPED as the requested + API (bit-identical), honestly documented as an interface convenience, not a hot-path win. + +REQUEST 3 -- skip the basis-width check in value(): mostly a NO-OP. decide() runs perceive_vec once, +so value() already receives a projected state and its width-check evaluates FALSE -- no redundant +projection happens. The only real saving is for a CONSOLIDATED brain handed a RAW state scoring +several actions: value_batch projects ONCE instead of once per action (measured 1.11x on that path). +Shipped as `_value_projected` (value() minus the branch, bit-identical) and folded into value_batch. + +REQUEST 2 -- cheaper auto_maintain: shipped as a caller-controlled BUDGET knob (grains / refresh on +auto_maintain, plus instance defaults), NOT an auto-gate. Measured findings: + - The proposed surprise auto-gate is UNRELIABLE: self.surprise is an EMA of reward prediction + error, which tracks reward NOISE as much as regime change, so it sat at 0.77 (floor 0.4) after + stable training -- it is not a "nothing shifted" signal. So the knob is caller-controlled. + - Savings are large: full 8-way 31 ms/tick; 1 grain + refresh (4 cand) 17 ms; 1 grain, no refresh + (2 cand) 9 ms; keep-only (1 cand) 3 ms. + - It is a speed/SELECTION-thoroughness trade, not free. Trimming fold grains is the safer lever + (both families still compete -- no missed-shift risk -- and here 1 grain picked the SAME memory + as 3, same held-out value); dropping the refresh family is sharper (it changed the selection + from a refresh to a preserve), so a stable courier that turns refresh off should periodically run + a full tick. Default (grains=(0.9,0.82,0.75), refresh=True) reproduces current behaviour exactly + -- the rescue-cracks canary passes unchanged. + +Tests: +6 (1021 -> 1027). test_holographic_creature_batch.py (value_batch / _value_projected +bit-identity on un-consolidated and consolidated-raw states; budget default-is-full and lean-trims). + +## Corridor planning (re-anchoring): plan() / replan_needed -- the way past the per-structure capacity cap + +A user stress-testing fleet turn-by-turn navigation hit a "capacity limit" on how many steps a set of +directions could hold. That limit is the HRR cliff, and it is a property of the ENCODING and the dimension, +not a holostuff wall -- and the fix was already in the box: + + - MEASURED cliff: a route stored as ONE undirected bundle (consecutive bind(tile_i, tile_{i+1})) decodes + only ~1 tile at dim 512, ~3 at 1024, ~5 at 2048 before crosstalk + the predecessor leak win. + - MEASURED fix: the SAME route as a DIRECTED structure (the permutation direction role, RAY-3) walked + with the throughput gate (RAY-1) decodes its full reliable prefix -- 15/15 at dim 512-1024 for a + 16-route, 23/23 at dim 2048. The cap is just that prefix length; more dim buys more. + +The way PAST the per-structure cap is re-anchoring, exactly Russian roulette for a decaying ray: don't push +one structure past its reliable depth -- bake a CORRIDOR (the next ~12-16 downhill steps, short enough to +decode cleanly), execute it, and re-anchor at the decision point. Arbitrarily long routes become a sequence +of cap-sized clean corridors; the brain is consulted once per corridor, not once per tile. + +holographic_plan.py is the API for that pattern, built ENTIRELY on existing tested pieces (holographic_directed ++ holographic_traverse): + - plan(start, field_step, max_steps, floor, action_of, is_branch) rolls out the goal field's downhill path + (field_step is the caller's gradient/flow/policy step; stops at is_branch or max_steps), bakes it as a + directed chain, and returns a Plan(memory, nodes, route, actions, throughputs, stopped, ds): the compact + plan hypervector, the decoded tile route, the decoded direction labels, and a per-step throughput. + - replan_needed(plan, executed, tile_ok, floor) is the cheap per-tick guard -- True (re-anchor) on + exhaustion / next-step throughput below floor / a blocked next tile; else execute the baked step. No + value() calls, no decode work. +Wired into UnifiedMind as plan() / replan_needed (general functionality belongs in the mind, not siloed in +the creature). KEPT NEGATIVE: a corridor that REVISITS a tile (a tight loop) can confuse cleanup, since two +steps map to near-identical vectors -- straight corridors to the next decision point are distinct by +construction; loops want segmenting. + +This is also the right answer to the batched-value request: baking a corridor collapses the ~72% trivial +straight-line steps into near-free executions of a baked plan, so the per-tick value() loop that batching +tried (and measured-failed) to speed up barely runs on those steps at all -- the brain only fires at the +decision points. value_batch remains the (bit-identical, no-speedup) API for the genuine decisions that +remain. + +Tests: +2 (1027 -> 1029). test_holographic_plan.py (selftest: at-cap corridor decodes fully, over-cap +reports only its reliable prefix, replan_needed gating) + a UnifiedMind integration test. + +## Creature-mind migration, Phase 0: the creature reaches the planning faculty + +The audit for the creature<->UnifiedMind migration (holostuff_creature_migration_plan.md) found the real +silo is ACCESS, not extraction: the creature's surface is almost all the RL layer (value/decide/remember/ +auto_maintain/...), it does not hoard general functionality, and UnifiedMind currently WRAPS it +(self._brain). The genuinely-general faculties (plan, directed_structure, traverse, recall, denoise) live in +the mind and the creature simply cannot call them. + +Phase 0 (behavior-preserving, shippable now) closes the part the navigation user needs: HolographicMind.plan +/ replan_needed, delegating to the SAME holographic_plan module UnifiedMind.plan uses -- so an NPC on a +creature can bake a corridor and re-anchor without the engine first inverting the creature<->mind +relationship. These two are substrate-level (operate on supplied vectors, not the creature's value memory), +so they add the capability with zero weight, no nesting (the creature does NOT build a UnifiedMind, which +would build its own nested brain), no circular import, and -- confirmed -- a bit-identical decision path (the +rescue-cracks canary passes unchanged). + +Phases 1-2 (invert the dependency so the creature is a layer ON UnifiedMind; optionally unify the encoder/ +memory onto the shared substrate, which is behavior-CHANGING and needs a canary re-baseline) are real +architectural commitments left for Moose to direct -- see the migration plan for the options (composition +recommended over inheritance for this shape), constraints (tie-sensitivity, the wide standalone surface, the +circular-wrapping problem), and sequencing. + +Tests: +1 (1029 -> 1030). test_creature_plan_bakes_a_corridor in test_holographic_creature_batch.py. + +## CreatureMind: the creature as a LAYER on the one mind (the architecture, made concrete) + +Moose's architecture, stated plainly: there is ONE mind (UnifiedMind) where all general functionality lives +-- the single encoder, the memory, recall, planning, denoising, the decision machinery -- and specialized +minds (creature behavior, image generation, ...) are thin LAYERS on top that inherit every faculty and add +only their domain wiring. The creature mind is the reference DEMO of that pattern. UnifiedMind's own encoder +docstring already declares the intent -- "this is the only encoder in the system; the memory and the brain +never encode anything themselves" -- and the standalone creature's separate CreatureEncoder is exactly the +deviation from it. + +holographic_creature_mind.CreatureMind(UnifiedMind) writes the target shape down: it subclasses UnifiedMind +(inherits the whole faculty suite), names its actions, and expresses the creature loop entirely over +inherited faculties -- `sense` is the one encoder's `perceive` in record mode (no separate encoder), `act` +is the inherited `decide`, `learn` is the inherited `reinforce`, and `plan` / `recall` / `denoise` are +there for free. So a CreatureMind is, in ONE object, a full mind that also acts, learns, and navigates -- +the specialization is a handful of convenience methods, nothing rebuilt. It is the template for any other +specialized mind: subclass UnifiedMind and wire your domain on top. + +MEASURED (selftest): a CreatureMind senses through the one encoder (bit-identical to perceive(...,'record')), +learns the rewarded action via inherited decide/reinforce, and bakes a corridor via the inherited plan +faculty -- all on one object. + +This sits BESIDE the standalone HolographicMind (the lower-level RL engine UnifiedMind still wraps today) +during the migration; the migration plan's later phases retire that duplication (one encoder, one memory) +and move the RL methods out of UnifiedMind's core into the layer -- a wide change touching the tests/tour +that call unified.reinforce/decide, with a canary re-baseline, since the creature is a teaching demo not a +frozen artifact. CreatureMind is where that migration lands, written down now so the destination is real. + +Also fixed in passing: plan()'s decode walked up to len(nodes)+2 steps, so a permissive floor let the +terminal node's noisy unbind oscillate PAST the corridor end ([1..6,5,6,5]); capped to exactly the +corridor's edge count (len(nodes)-1), the decode never invents tiles beyond what was rolled out. + +Tests: +1 (1030 -> 1031). test_holographic_creature_mind.py (CreatureMind selftest: one encoder for senses, +act/learn on inherited machinery, inherited planning -- the layer-on-one-mind pattern). + +## MEASURED: is the bespoke creature value memory a redundant old path? -- No. (a kept result + negative) + +The long-running confusion was whether HolographicMind's prototype value memory is a "tumor" duplicating the +unified mind, or an essential component. Audit first: HolographicMind imports only the shared kernel +(bind/bundle/permute/Vocabulary), and through UnifiedMind its ENCODING already goes through the one encoder +(decide/reinforce call perceive; the creature encoder is bypassed) and the calibrated honesty layer is wired +in. The one thing genuinely separate is its value MEMORY (per-action prototype banks + soft k-NN returns +regression) -- and the modern advancements (Hopfield cleanup, resonator, denoise) address clean recall / +factorization, not value regression, so it was not obvious they would help it. + +So we measured it instead of guessing (exp_value_memory.py). Task: 16 egocentric situations (4 food dirs x 4 +distractor combos, best action = move toward food), train on 12, test greedy accuracy on all 16 (in-sample + +held-out generalization). All learners handed the SAME perceive-encoded vectors (encoder not a variable), +SAME episode stream, SAME exploration -- only the value memory differs. 6 seeds, dim 512. + + RESULT (greedy accuracy, chance 0.25): + bespoke per-action memory : in 0.96 gen 0.75 + unified-memory, hard class: in 0.57 gen 0.21 (NEGATIVE -- naive replacement fails) + unified-memory, soft k-NN : in 0.57 gen 0.25 (NEGATIVE -- mechanism-matched rival ALSO fails) + +The bespoke memory wins decisively, and -- the important negative -- the gap is NOT just soft-vs-hard: a soft +k-NN value regression built on the unified SelfOrganizingMind (the fair, mechanism-matched rival) still +collapses to chance generalization. The bespoke's edge is its PER-ACTION prototype organization: action a's +bank holds every state where a was taken, so value(s,a) is a soft k-NN Q-value regression over similar states +across ALL situations -- which generalizes ("states like this, when you went east, paid off"). The +situation-class + value-table structure first buckets the state into a class (losing the cross-situation +regression), and even soft-weighting over classes does not recover it (it only approaches the bespoke at a +tuned high novelty_floor: floor=0.7 gives soft gen 0.67, still under 0.75 and floor-sensitive). + +VERDICT: the value memory is a deliberate, measured-essential RL engine, not a redundant old path. Keep it. +The remaining "confusion" to fix is only the SURFACE: the public pattern for an agent is +CreatureMind(UnifiedMind) (not building from HolographicMind directly), and the one real duplication left is +the standalone CreatureEncoder still used by navigator/moe/lookahead/core/app -- a minor cleanup (route them +through the mind's encoder), not an excision. The measurement saved a wrong refactor; the negative is kept. + +Tests: +0 (no faculty changed; HolographicMind docstring updated to record the verdict, exp_value_memory.py +kept as provenance). + +## Follow-up: is the standalone CreatureEncoder a stray duplicate? -- No (the value-memory lesson, again) + +After the value-memory result, the last "loose thread" looked like the standalone CreatureEncoder used +outside UnifiedMind. Looking closely (not assuming) settled it the same way: it is NOT redundant. Through +UnifiedMind the encoding already goes through the ONE encoder (perceive; the brain/memory never encode +themselves), so the rule holds where stated. CreatureEncoder is the creature DOMAIN's encoder: role/filler +binding (the shared primitive) PLUS (a) build_state's action-memory -- a working memory of recent moves +perceive has no notion of (app's maze console uses it) -- and (b) the `seen` role/value tracking that +HolographicMind.describe REQUIRES to decode a state back into sense terms. The rescue canary is tie-sensitive +to its exact output (the kept-negative in encode(): a 1e-16 change flips the trajectory). And the modules +reuse it ON PURPOSE: navigator is an explicit "inception" demo (same brain + same encoder, new world), not +accidental duplication -- and it is three modules (navigator/lookahead/app), not the five I'd loosely said +(moe/core use only the engine, never the encoder). + +So, like the per-action value memory, CreatureEncoder earns its place; routing those uses through perceive +would drop describe + action-memory and re-baseline the whole tie-sensitive suite. Corrected the docstrings +(HolographicMind + CreatureEncoder) to say this accurately instead of mislabeling it "duplication to tidy." +The genuine, low-value, optional leftover is only the ~2-line role/filler bind+bundle that appears in both +encoders; factoring it to a shared helper is cosmetic and touches the tie-sensitive path, so it is not done. + +Tests: +0 (docstrings/comments only; no behavior changed). + +--- + +## plan_route: a whole arbitrarily-long route in one call, by chaining cap-sized corridors + +A delivery-game user kept hitting the ~15-tile cap and reported it "still exists." They were right about +the surface fact and wrong about its scope, and the distinction matters: the cap bounds ONE baked structure, +not the route you can navigate. Measured the two paths at dim 512 on a 45-tile straight route: + + * cram all 45 tiles into ONE plan(max_steps=45): the directed structure is overstuffed and the decode + COLLAPSES -- it came back with **1** step, not a clean 15-prefix. The cliff is steeper the more you cram; + "~15" is the reliable depth for a corridor-SIZED structure, not a floor you get for free at any length. + * chain cap-sized corridors, re-anchoring at each leg's reliably-decoded end: the full **44-step** route + decodes EXACTLY. Re-anchoring resets the HRR accumulation each leg, so each leg stays inside its capacity. + +This is the same move plan() + replan_needed already enable (bake a corridor, drive it, re-anchor on the +gate) -- but a user calling plan() ONCE and expecting the whole route hits the wall. plan_route runs that +loop internally: it chains plan() corridors, re-anchors at `nodes[route[-1]]` (the last RELIABLY-decoded +tile -- never past it, so a short-but-clean leg just re-anchors sooner), breaks on field_end/branch only +when the decode actually reached the leg's last tile, and caps the whole route at `max_total`. Returns a +Route (full action sequence, the chained corridors, stop reason, re-anchor count, step total). Wired as a +method on UnifiedMind AND HolographicMind (delegating to the module, like plan/replan_needed); CreatureMind +inherits it. Verified: 39/40-tile routes decode exactly through all three minds. + +KEPT NEGATIVE: `corridor` must stay at/under the dim's reliable decode depth (default 14, safe at dim +512-1024). Set it too high and that leg overstuffs its OWN structure -- the same cliff, per leg: measured +corridor=30 at dim 512 does NOT recover the full route (it skips/corrupts tiles). plan_route does not, and +cannot, rescue an over-long leg; it only removes the cliff by keeping each leg small. The realtime courier +still wants plan() + replan_needed (bake-as-you-go, reacts to traffic); plan_route is for getting the WHOLE +route in hand at once (display / validate / pre-plan a leg). + +Tests: +4 (1031 -> 1035). test_holographic_plan.py grew the selftest (45-tile chained route exact vs a +collapsed single plan; max_total prefix; over-long-corridor negative) and added three named API tests; +test_integration.py added a plan_route-through-the-mind test. Files: holographic_plan.py (plan_route + Route), +holographic_unified.py / holographic_creature.py (the wired method), test_holographic_plan.py, test_integration.py, +tour.py. + +--- + +## chunk_route: the explicit-sequence twin -- scaling to GPS/experiment size by chunking + +The capacity question came back as a scaling worry: would the ~15 cap make holostuff useless for GPS +navigation or a long experiment plan? Measured the answer rather than asserting it. A 200-step route at +dim 512: crammed into ONE structure it decodes 1 step (the cliff); chunked into <=14-element pieces with +re-anchoring it replays all 199 steps EXACTLY in 15 chunks, in 52 ms. So the worry is unfounded -- chunking +makes EFFECTIVE length unbounded at LINEAR cost (~N/14 pieces), and each piece is one compact vector. The +per-piece cap is physics (a fixed-width structure can't hold unbounded order, like any bounded buffer); +chunking is the standard, correct workaround, not a hack. + +The thin orchestration layer this needs already half-existed: plan_route (prior section) chunks a route you +DISCOVER by following a goal field. The genuine gap was the EXPLICIT case -- a sequence you ALREADY HAVE (GPS +waypoints from a planner, a scientist's fixed protocol, any known list). You had to hand-write a nearest-match +field_step to feed plan_route. chunk_route closes that: hand it the ordered list, it splits by position into +<=chunk pieces (overlapping by one element so each piece re-anchors exactly where the last ended -- nothing +skipped or double-counted), bakes each as a clean directed structure, and returns the full replayable sequence +plus the chunk vectors. It is implemented directly on build + gated_traverse (not by wrapping plan_route), so +it does not depend on nearest-match rediscovery -- it knows the order and chunks it. Wired on UnifiedMind AND +HolographicMind; CreatureMind inherits. Verified: a 200-element list replays exactly through all three. + +The relationship to the rest: this is the third chunking mechanism in the engine, one per kind of long thing. +Routes you discover -> plan_route. Sequences you hold -> chunk_route. Programs that outgrow one structure -> +HoloMachine define/CALL (sub-programs called from a short top-level program). All three are the same lesson -- +keep each structure inside its capacity and coordinate the pieces -- which is the project's recurring theme of +beating a hard limit with composition rather than pretending the limit isn't there. + +KEPT NEGATIVE (same shape as plan_route's): `chunk` must stay at/under the dim's reliable decode depth (default +14, safe at dim 512-1024); an over-long chunk overstuffs its own piece -- the cliff, per chunk. And the elements +must be DISTINGUISHABLE (a codebook): a sequence that revisits the same element can confuse a chunk's cleanup, +since two steps map to near-identical vectors. chunk_route removes the cliff by keeping pieces small; it cannot +rescue an over-long piece or a non-distinguishable alphabet. + +Tests: +3 (1035 -> 1038). test_holographic_plan.py: chunk_route replays an explicit 200-step sequence exactly +in ~15 compact chunks, and degenerate (empty / single-element / fits-in-one-chunk) inputs are safe; +test_integration.py: a 200-step explicit sequence through the mind. Files: holographic_plan.py (chunk_route), +holographic_unified.py / holographic_creature.py (the wired method), test_holographic_plan.py, test_integration.py, +tour.py. + +--- + +## run_chunked: VSA programs past the single-program cap (chunking transfer backlog, item P1) + +First build off the chunking-transfer sweep. The sweep's headline question -- does the chunk-and-re-anchor +lesson unlock more complex VSA programs? -- measured YES, with a load-bearing negative. A 60-instruction +HoloMachine program at dim 1024 (single-program cap ~20-32) decodes to garbage as one structure (cosine 0.08 +to the intended bind-chain). The OBVIOUS fix -- factor it into define()d functions and CALL them -- ALSO +FAILS (cosine 0.06): CALL pulls each sub-program out of a BUNDLED library, and bundling several +function-vectors into one library vector re-introduces the very cliff (the docstring's "busy disk" +crosstalk). The fix that works is the true chunk_route analog: each chunk is its OWN clean program vector and +the HOST threads the accumulator across them -- 60 instructions then run at cosine 1.000. + +Shipped HoloMachine.run_chunked(program, chunk=14, ...): strips a trailing HALT, splits into <=chunk pieces +(never ending a chunk on IFMATCH/REPEAT so a gate/repeat stays with the instruction it targets), assembles +each piece to its own vector, runs them threading the ACC, and stops the whole run if a mid-program HALT +fires. Returns (acc, trace) like run(), trace concatenated. Verified exact on three different long programs +(60 binds; 50 binds different phase; 40 mixed bind/bundle/permute), bit-equivalent to run() on a short +program, control-construct-intact at a forced chunk=1 seam, and mid-HALT stops the run. + +KEPT NEGATIVE / the operand-dependent edge: the chunk size must be WELL UNDER the cliff, not on it. At dim +1024 the decode is solid through ~18 instructions but turns OPERAND-DEPENDENT right at ~20 -- a 20-instruction +chunk decoded for two operand sequences and FAILED for a third in the same program (cosine 0.448). This is +the same lesson as plan_route's over-long-corridor negative, sharper: near the edge, success depends on the +specific operands, so the default 14 leaves deliberate margin. The reliable length grows with dim (chunk=20 +is solid at dim 2048+), so raise chunk at higher dim. And the CALL-the-library route is kept as a test +(test_call_library_does_NOT_chunk_a_long_program_kept_negative) so nobody reaches for it. + +run_chunked is a HoloMachine method, NOT a UnifiedMind faculty -- the VM stays adjacent to the mind (the +integration plan's standing decision), so there is no UnifiedMind wiring or integration test, only the +machine's own tests. The user guide (writing_vsa_programs.md) gained a "Running a program past the cap" +section and the limits section was corrected (the old "factor into functions" advice was the measured +negative). + +Tests: +4 (1038 -> 1042). test_holographic_machine.py: run_chunked past the cap (60 instr exact vs a +collapsed single program), equivalence to run() on a short program, constructs-intact + mid-HALT, and the +CALL-library kept negative. Files: holographic_machine.py (run_chunked), test_holographic_machine.py, tour.py, +writing_vsa_programs.md, README (test-list entry + counts). + +--- + +## RouteIndex: sub-linear random access into a chunked route (chunking transfer backlog, item X3) + +Second build off the chunking-transfer sweep, Pharr's acceleration-structure angle. A long route is now many +chunks (plan_route / chunk_route), and "where am I on it?" should be a jump, not a replay from the start. +RouteIndex is a BVH over the chunks: index each chunk by a SUMMARY vector (the bundle of its tiles), then +locate a query two-level -- nearest chunk summary (level 1), then nearest tile within that chunk (level 2). +Measured on a 200-tile route: 200/200 tiles located exactly at ~28-30 comparisons per query vs 200 for a flat +scan (~6.9x fewer). Why the bundle summary is a usable index: a tile in a chunk has cosine ~1/sqrt(chunk_size) +to that chunk's summary (it is one of its components) and ~0 to the others, so argmax over summaries is the +right chunk -- the same bundle-crosstalk that CAPS a single structure is what makes the summary discriminative +here. The global_step the locate returns accounts for the one-tile overlap between chunks, so it recovers the +true route index exactly (verified g == t for sampled tiles). + +Shipped holographic_plan.RouteIndex(route): precomputes normalized chunk summaries and per-chunk start offsets +in __init__; .locate(query) -> (chunk, position_in_chunk, global_step); .n_chunks. Built once, queried many -- +the courier asking its position every tick is the repeated-query case this amortises. Wired as m.index_route( +route) -> RouteIndex on UnifiedMind AND HolographicMind; CreatureMind inherits. Empty route is safe (returns +(-1,-1,-1)). Verified locating across all three minds (tile 137 -> chunk 9, pos 11, step 137, exact). + +Tests: +3 (1042 -> 1045). test_holographic_plan.py: RouteIndex locates every tile in the right chunk at the +exact position with sub-linear comparisons, and the empty-route case is safe; test_integration.py: random +access through the mind. Files: holographic_plan.py (RouteIndex + the bundle/cosine import), +holographic_unified.py / holographic_creature.py (index_route), test_holographic_plan.py, test_integration.py, +tour.py, README (test-list entry + counts). + +--- + +## Determinism / tie-break audit of the chunking seams (chunking transfer backlog, item C2) + +Macklin's discipline applied to the three seams just added to the plan module (plan_route, chunk_route, +RouteIndex): a bit-exact change must stay bit-exact, and a query must never resolve on a knife-edge tie. The +audit came back CLEAN -- no fix needed, so the deliverable is a regression test that locks the property in. +Measured: chunk_route and plan_route produce identical actions AND bit-identical chunk vectors run-to-run at a +fixed seed; RouteIndex summaries are bit-identical and .locate is deterministic; a deliberately ambiguous query +(equidistant between two tiles in different chunks) resolves the SAME chunk every call (numpy argmax breaks ties +by lowest index); and 1e-12 perturbations of a tile query flipped the locate 0/200 times (the tiles are +well-separated, so a query near a real tile has a clear winner -- not tie-sensitive). This is expected -- the +seams are built on build / gated_traverse / bundle / argmax, all deterministic given the seed -- but it is the +exact class of bug the bind_batch lesson warns about, so it is now asserted, not assumed. + +Tests: +1 (1045 -> 1046). test_holographic_plan.py: test_chunking_seams_are_deterministic_and_not_tie_sensitive. +Audit-only; no faculty changed. Files: test_holographic_plan.py, README (counts). + +--- + +## Chunked sequence memory: order queries exact past the single-bundle cap (chunking transfer backlog, item S3) + +Third build off the chunking-transfer sweep, Plate/Olshausen's positional-encoding angle. SequenceMemory stores +order as position = rotation, bundled (element i is permute(atom, i+1), all summed into one vector), and answers +step / position_of / precedes / validate by un-rotating a position and reading it off. That single bundle caps +with length -- but FAR more gracefully than the directed-chain route did. Measured at dim 2048, vector-only +positional decode accuracy: ~100% at length 50, ~96% at 100, 69% at 200, 29% at 400, 15% at 800 (the route's +directed structure, by contrast, collapsed to one step at ~45 tiles -- iterative traversal compounds error, +direct positional read does not, so the positional encoding is the more robust of the two). add(..., chunk=K) +stores the sequence as positional blocks of <=K, each its own clean bundle, and routes a position query to the +one block it lives in (divmod(i, K)); measured chunked accuracy is 100% at EVERY length tested. Gain grows from ++0% (short) to +85% at N=800 -- load-bearing for long sequences, a pure no-op on short ones. + +IMPORTANT, narrower than P1/X3: step (position -> element) cleans against the KEPT element list, so it is always +exact regardless of bundle quality -- the chunking benefit is NOT visible there. The win shows in the ORDER +queries that decode positions FROM the vector: precedes and position_of (and vector-only step against the full +vocab). Those are exactly SequenceMemory's distinctive value -- the recipe-vs-pile-of-steps relation -- so the +gain lands where it matters, but the framing has to be honest about which queries it helps. Same recurring +negative as the other chunkers: K must stay at/under the dim's reliable bundle length, or each block hits the +same cliff (default margin 14). + +Shipped backward-compatibly: storage went from a 2-tuple (vector, elements) to a 3-tuple (repr, elements, chunk) +-- index 1 (the element list, read by app.py and the mind) is unchanged, chunk=0 is the original single-vector +path, and nothing external reads index 0. add gains chunk=0; a _probe(repr, chunk, i) helper centralizes the +block routing so step / position_of / precedes / validate each route through it. Wired through the mind as +learn_plan(name, steps, chunk=K) -- step_at / precedes / validate_plan are automatically chunk-aware; verified a +200-step protocol exact through the mind where a single bundle slips (tour: 200-step plan precedes 22/33 single +-> 33/33 chunked). + +Tests: +3 (1046 -> 1049). test_holographic_sequence.py: chunked storage keeps long-sequence order queries exact +and is a no-op on short ones; backward-compatible default storage shape. test_integration.py: learn_plan chunked +keeps a long protocol exact through the mind. Files: holographic_sequence.py, holographic_unified.py, +test_holographic_sequence.py, test_integration.py, tour.py, README (test-list entry + counts). + +--- + +## Where chunking helps -- and where it doesn't: the S1 overlap-add negative (chunking transfer backlog, item S1) + +S1 was the most seductive item in the chunking-transfer sweep: chunk_route's one-element boundary overlap looks +exactly like the phase vocoder's weighted overlap-add, so processing a long signal as overlapping windowed +chunks "should" be the same chunk-and-re-anchor win. Prototyped on the FPE substrate (VectorFunctionEncoder: a +continuous function f is the bundle f = sum_i y_i encode(x_i), read by an inner product = kernel sum). It is a +clean MEASURED NEGATIVE -- chunking is not just a no-op, it is HARMFUL. + +Measured (raw inner-product readout, shape correlation vs the noise-free designed-kernel sum): a SINGLE bundle +reconstructs the function with corr ~1.0 at every domain length tested -- N=120 (0.91), 400 (1.00), 800 (1.00), +1500 (1.00) -- while hard-cut chunking and proper Hann overlap-add both sit at corr ~0. The reconstruction does +NOT degrade with domain length, so there is no capacity problem for chunking to solve, and breaking the global +kernel sum into windows only introduces boundary-incomplete neighbourhoods and per-window normalisation error. + +WHY the rhyme fails -- and the principle it buys. FPE codes are shift-invariant powers of ONE base, so + is the SAME kernel for every pair at a given distance: the finite-dimension error is a +DETERMINISTIC sidelobe of that kernel, not a √N pile of independent random noise. And the readout = sum_i w_i distributes over the superposition EXACTLY (linearity). So +the kernel sum is computed exactly, the kernel decay localises it, and a longer domain changes nothing about the +local readout. Contrast the route / sequence / program: there the task is to DECODE a specific item back out of +a superposition by cleanup, and every other item's crosstalk eats into that recovery -- which is precisely what +caps, and precisely what chunking bounds. The sharpened rule: + + Chunking helps DECODE-VIA-CLEANUP -- recover/identify a SPECIFIC item from a superposition, where the other + items' crosstalk caps recovery (routes, sequences, programs: plan_route, chunk_route, run_chunked, chunked + SequenceMemory all live here). + Chunking does NOT help LINEAR-FUNCTIONAL EVALUATION -- evaluate (a kernel-density / + function query), which is exact by linearity regardless of how many terms are bundled (FPE function readout + lives here). + +This reconciles cleanly with the pre-existing FPE capacity cliff (test_capacity_cliff_is_a_kept_negative): that +cliff measures absolute DETECTION separation (is THIS point placed vs empty, cosine-normalised), which decays as +the bundle norm grows with K -- a detection/decode behaviour. The S1 metric is relative SHAPE fidelity, which is +preserved. Both true; they are different behaviours of the same bundle, and the decode-vs-evaluate split is what +separates them. The same caution likely weakens S2 (overlapping-block denoising of a long signal) -- aggregate +block denoising is closer to evaluation than to per-item decode -- and is flagged in the backlog to be checked +before any build. + +Tests: +1 (1049 -> 1050). test_holographic_fpe.py: test_function_shape_reconstruction_does_not_cap_so_overlap_ +add_chunking_is_a_no_op (pins the corr ~1.0 evidence). No faculty built -- this is a recorded negative that +sharpens the theory of the whole chunking arc. Files: test_holographic_fpe.py, holostuff_chunking_transfer_ +backlog.md (S1 marked negative + build order updated), README (count). + +--- + +## Does chunking help text / image generation? Tiled splat scenes (chunking transfer backlog, item X2) + +Asked whether the chunking arc transfers to generation. Settled it with the decode-vs-evaluate principle (from +the S1 negative) plus a code audit and a measurement, and the answer splits cleanly. + +TEXT generation: NO. The generators (generate -> n-gram, generate_structured -> steered_generate) condition each +step on a BOUNDED context -- the predictor's n-gram order plus the last `lookback` tokens -- so a longer +generation never piles into a capping superposition. There is no decode-from-a-long-bundle cliff for chunking to +fix; generating length 30 vs 300 uses the identical per-step context. (Long generations can still drift or loop, +but that is bounded MEMORY, not a capacity cliff -- fixing it would need a hierarchical long-range summary, a +different and speculative mechanism, not this lesson.) + +IMAGE: YES, in the content-addressable splat SCENE. splat_bundle encodes a scene as grid*grid bind(cell_role, +occupancy_level) terms in ONE hypervector, and recall_region reads a cell back by unbind + cleanup -- a textbook +decode-via-cleanup readout. So as the grid gets finer the bundle's own crosstalk grows and region recall caps: +measured at dim 4096, accuracy is ~100% at grid 8, 98% at 16, 88% at 24, 75% at 32. This is the SAME cap chunking +bounds for routes / sequences / programs, and the chunk here is a TILE. splat_bundle_tiled routes each cell to a +tile bundle (floor-divide the grid index by `tile`), so a tile holds at most tile*tile bindings no matter how +fine the TOTAL grid is -- the per-bundle load is fixed and recall holds ~100% at any resolution (measured 75% -> +100% at grid 32). Costs one hypervector per tile (proportional storage, the price of exceeding a single vector's +capacity -- the same trade chunk_route and chunked SequenceMemory make). This is the splat side of backlog item +X2, and it confirms the principle predicts WHERE the lesson lands: image RECALL/representation that decodes from +a superposition (yes), not text generation with bounded context (no), and not the FPE function readout that is +linear-exact (S1, no). + +NOTE the precise complement already in the box: SplatArchive.region stores splats as an EXPLICIT list and is +exact per-splat -- so it never had this cap. The tiled bundle is the COMPACT, content-addressable, coarse-but- +robust path; tiling is what lets it stay accurate at fine resolution. + +Shipped: holographic_splat.splat_bundle_tiled / recall_region_tiled (global cell roles, so recall needs no +remapping; routes a cell to its tile bundle and reuses recall_region's unbind+cleanup). Wired onto UnifiedMind +as splat_scene(field, grid, tile, levels, k) -> tiled scene and splat_region(scene, cell) -> occupancy. +Determinism-clean (tile bundles bit-identical run-to-run) and empty-tile safe. + +Tests: +4 (1050 -> 1054). test_holographic_splat.py: the single-bundle cap (negative, acc<0.85 at grid 32), +the tiled fix (acc>0.99 at grid 32), determinism + empty safety. test_integration.py: splat_scene region recall +exact at fine resolution through the mind. Files: holographic_splat.py, holographic_unified.py, +test_holographic_splat.py, test_integration.py, tour.py, README, holostuff_chunking_transfer_backlog.md (X2). + + +## StructuredIndex -- the shared content-address index (and where Merkle already lives) + +Three places were independently growing the same primitive: "given a pile of vectors, find the one this query +points at, without scanning all of them." RouteIndex (a flat two-level summary scan), a chunked sequence, and +the content store (which already grows a per-bucket HoloForest). The request was to stop duplicating it -- one +abstraction the rest draw from, so a future caller does not re-hit the same limit by re-inventing the lookup. + +StructuredIndex (holographic_tree.py) is that, as a thin payload-carrying wrapper over the HoloForest RP-tree: +build(keys, payloads); locate() is the sub-linear path with a free cross-tree agreement/abstention signal; +locate_k() is sub-linear top-k; locate_exact() is the flat guaranteed-nearest. The payload is the point -- you +file vectors and get back a LABEL (a URI for the store, (chunk, step) for a route, a step index for a sequence), +not a row number. Wired as the mind faculty structured_index(keys, payloads). + +TWO RULES ARE BAKED INTO IT, both measured the hard way in the design probe so a future caller meets them as +documentation rather than rediscovering them as "limits": + 1. Key on the ITEMS THEMSELVES. A hyperplane tree only routes a query to the right leaf when query ~= key. + Filing items under a bundle-SUMMARY the query is weakly correlated with mis-routes them -- a tile has cosine + only ~0.27 to its chunk summary, which an exhaustive argmax still resolves but a greedy tree descent does + not (measured: locating a route by chunk-summary THROUGH a tree collapsed to ~1/200, while the same tree + over the tiles themselves routed home). This is the decode-vs-evaluate constraint wearing a routing hat. + 2. Never store the index as a BUNDLE. Superposing the keys and recovering one by unbind+cleanup is decode-via- + cleanup and caps with set size (measured: 200 -> 127 -> 15 recovered as the set grows). The index must be a + navigable STRUCTURE (this tree) or, below the crossover, an explicit scanned list -- never a superposition. + +HONEST CROSSOVER (kept, not hidden): the forest carries a large fixed constant (n_trees x leaf_size x beam +candidates), so a flat scan WINS until the set is in the low thousands -- measured ~30 vs ~470 comparisons for a +few-hundred-chunk route, the forest only pulling ahead past ~6000 items. So locate_exact is not a fallback, it is +the correct call below the crossover; RouteIndex's flat scan is therefore this index at its small-n operating +point (exact, cheap), and the content store's HoloForest is it at its at-scale one. The abstraction unifies them +without a regressive refactor -- the working flat path stays, and new callers reach for the one primitive. + +MERKLE (the question that prompted checking, and the payoff of checking): holostuff ALREADY has a holographic +Merkle tree -- holographic_verify.CompositionTree, mind faculty verify_store. leaf = bind(pos, item), node = +bundle(children), root = the commitment; detect by rebuilding and comparing the root, localise a changed item in +<= log2(n) composite comparisons. Its kept negative is the right caveat: the root is LINEAR, so collisions exist +and a key-aware adversary can cancel a change by deconvolution -- evidence of ACCIDENTAL corruption, NOT +cryptographic tamper-proofing. The clean separation: StructuredIndex is for LOOKUP (recover an item), the Merkle +tree is for INTEGRITY (has anything changed, which one). And they sit on opposite sides of the decode-vs-evaluate +line -- comparing two whole composites by cosine is an EVALUATION, which does not cap, which is exactly why the +Merkle tree's detection survives at any store size while a lookup that must DECODE an item does not. + +Tests: +7 (1054 -> 1061). test_holographic_tree.py: sub-linear content routing, payload labels, exact+flat scan, +ranked top-k, the agreement signal, payload/key mismatch guard. test_integration.py: one structured_index faculty +serving both content (payload=URI-like) and route (payload=(chunk,step)) lookups. Files: holographic_tree.py, +holographic_unified.py, test_holographic_tree.py, test_integration.py, tour.py, README. + + +CHUNKING-TRANSFER, THE LAST THREE (X1 tiled scene factorization, C1 chunk dedup, R1 re-anchored rollout): +the sweep that asked "is the recent route lesson a general capacity primitive?" closes here with two builds and +one kept negative, all from the same decode-vs-evaluate test -- does the operation DECODE a specific item from a +superposition (helped by chunking/tiling/re-anchoring) or merely EVALUATE a linear form (not helped, because a +linear map has no capacity cliff to relieve). + +X1 (WIN). A multi-object scene is a superposition the resonator must FACTOR -- a decode-via-cleanup, so it has the +capacity problem tiling addresses, and unlike S1 the S1 negative does not pre-empt it. Measured: at dim 1024 the +whole-scene factorization caps at ~5 objects and collapses past it (~30% recovery at 15 objects across 8 seeds); +splitting the objects into spatial tiles of <= cap each, factoring every sub-scene, and merging lifts recovery to +~93%. The tile size plays the chunk's role exactly: it must stay at/under the per-tile cap or each tile re-hits +the same cliff (tiles of 5 at dim 1024 still leave a little within-tile crosstalk -- 10-15/15 per seed -- which a +smaller tile or higher dim removes). This is chunk_route's move and splat_bundle_tiled's move on the resonator: +beat a fixed structure's capacity with composition, at the honest price of keeping the tiles. SceneCoder. +factor_scene_tiled / mind decompose_scene_tiled. + +C1 (WIN, with its honest bound). A long route that REVISITS the same corridor, or a program with repeated motifs, +stores the same compact chunk vector many times. Content-address the store -- keep each unique chunk once and +replace repeats with a reference -- and storage shrinks by EXACTLY the repetition ratio: measured 65% on a +17-corridor loop with 6 distinct chunks, 0% on a no-repeat control (dedup can only save what actually repeats), +references rebuilding the original sequence bit-for-bit. This is the storage twin of StructuredIndex: that finds +an item BY content, this stores items BY content so identical ones coalesce -- and comparing whole chunk vectors +by cosine is an EVALUATION (not a decode), so two genuinely distinct chunks never collide at high dim, no cap. +holographic_plan.dedup_chunks / mind dedup_chunks. + +R1 (KEPT NEGATIVE, joining S1). Re-anchoring a learned propagator's long rollout onto the consolidation manifold +does NOT help -- and the reason is the same line. A route's per-hop cleanup ACCUMULATES crosstalk, so re-anchoring +rescues it; a linear propagator rollout is repeated application of one operator, an EVALUATION. Measured on a +trajectory in the propagator's exact model class (per-frequency phase advance == circular convolution, the audio +sweet spot): the free rollout's drift is ~0 over 50 steps -- the operator TRACKS the trajectory, there is no drift +to fix -- and projecting the state onto the rank-r training-state manifold every few steps only makes it worse +(mean drift 0.0001 -> 0.5+), because the manifold of training states is a SUBSET of where the true trajectory goes +and re-projection discards valid forward signal. On a trajectory OUTSIDE the model class the prediction is wrong +within the manifold (a phase error), which manifold projection cannot fix either. So re-anchoring helps +decode-via-cleanup chains (routes, sequences, programs), never a linear-operator rollout. Pinned in +test_holographic_dynamics.py so nobody "fixes" a non-problem. + +The whole 13-item sweep is now resolved: P1/X3/S3/C2 shipped, X2/X4 shipped (X4, the multi-terminal Tero "Tokyo +rail" network design, was already on disk), S1/R1 kept negatives, X1/C1 shipped here. The decode-vs-evaluate line +predicted every outcome: chunking/tiling/re-anchoring helps wherever an item must be DECODED from a superposition, +and is inert (or harmful) wherever the query is a linear EVALUATION with no capacity cliff. + +Tests: +6 (1061 -> 1067). test_holographic_scene.py: tiled scene factorization beats the capped whole scene across +seeds. test_holographic_plan.py: dedup saves at the repetition ratio + exact rebuild, and saves nothing without +repetition. test_holographic_dynamics.py: re-anchoring a rollout does not help (R1 kept negative). test_integration +.py: the dedup_chunks and decompose_scene_tiled faculties through the mind. Files: holographic_scene.py, +holographic_plan.py, holographic_unified.py, test_holographic_scene.py, test_holographic_plan.py, +test_holographic_dynamics.py, test_integration.py, tour.py, README. + + +SCHEMA-GUIDED TYPED PLANS + descend (the structured branching output the planning work was missing): +a colleague who hit the route/planning case asked for a first-class Plan/PlanNode type (primary action, named +contingency branches, scope, confidence) plus a decode helper, so the bind/bundle tree is not re-derived by +hand each time. The audit found the ENCODING already proven (holographic_typed.encode_tree / StructureRecipe lay +down exactly this role-filler tree, bit-exact) and a NAME collision (holographic_plan.Plan is the corridor +planner's namedtuple). New module holographic_planshape.py ships the type and the decode. + +THE LOAD-BEARING IDEA (why a user-provided SHAPE is the right design, not sugar): the shape IS the decode key. +Decoding a foreign vector with no shape is the resonator's blind, crosstalk-bounded parse (decompose_structure), +which caps -- the typed module says so itself, a StructureRecipe is a GENERATOR not a parser. With the shape +KNOWN, decode is a deterministic walk: unbind exactly the roles the shape names, clean against exactly the +codebooks it gives, recurse on exactly the field it marks recursive. No search. The decode-vs-evaluate line +again -- a known structure turns a decode-SEARCH into clean unbinds, so a 3- or 4-level plan round-trips every +action and scope EXACTLY where the blind parse would crater. Measured: per-node branch fan-out holds past 16 at +dim 1024 (the cap is the per-node bundle width, the HRR capacity bound -- a huge node nests, the same lesson). + +WHAT SHIPPED. encode_record / decode_record: the GENERAL "bring your own shape" path -- a flat record of named +symbolic fields (a scientific decision record, a classified state) <-> one vector, decoded against per-field +codebooks. PlanNode + PlanShape + encode_plan / decode_plan: the concrete contingency tree and its schema-guided +round-trip. descend(vec, situation, shape): the walk to the branch matching the current situation -- the genuine +generalisation of the machine's IFMATCH from one gated instruction (fire iff cosine(state, x) >= tol) to a named +branch tree, matching the situation against branch condition keys by cosine and ABSTAINING (returning the node's +primary action -- "no contingency applies") when none clears a MEASURED noise floor. Mind faculties: plan_shape, +encode_plan, decode_plan, descend, encode_record, decode_record. + +PANEL GROUNDING (seats + real published methods, no fabricated opinions). Plate (HRR): recursive role-filler +binding with per-level normalisation (bundle already does it); the per-node fan-out cap is his capacity bound, +kept as the negative. Olshausen (resonator networks): the resonator is the UNKNOWN-structure tool the schema +path AVOIDS -- it stays the fallback. Togelius (game AI): descend is a behavior-tree selector ("best child whose +condition fires, else fall through"); the decodable, readable tree is the explainability his field wants. +Cranmer (calibrated detection): the branch gate is a MEASURED noise floor and confidence is the MEASURED decode +cosine, not a magic threshold (a simple cousin of RecallNull; upgrade to the full null for a controlled +false-match RATE). Macklin (bit-exact tie-breaks): argmax ties broken deterministically, encode and descend +run-to-run identical (a determinism test pins it). Eno (the reframe): the output shape is not formatting, it is +the choice of which structure to impose, which determines what is recoverable -- so the shape is a first-class +decode key, optional (omit it and you are back to the resonator's discovery). + +KEPT HONESTLY. confidence rides on the PlanNode OBJECT (builder metadata); it is NOT encoded into the vector, +because forcing a scalar into the bundle decodes lossily and would betray the honest-number rule -- decode +instead fills the returned node's confidence with the measured decode cosine. And schema-guided decode NEEDS the +schema: a truly foreign vector of unknown shape falls back to the capping resonator. This is explicitly "decode +a structure you have the shape for", which is the plan/protocol case -- exactly why providing the shape is the +right design, not a limitation of it. + +Tests: +11 (1067 -> 1078). test_holographic_planshape.py: the module self-test, flat-record round-trip + +measured per-field confidence, plan round-trip exact, a deep tree held by schema guidance, descend walking to the +right branch, descend abstaining when no branch applies, descend matching a state VECTOR (and abstaining on an +unrelated one), and determinism of encode/descend/noise-floor. test_integration.py: the plan faculties +(encode_plan/decode_plan/descend) and the general record faculty through UnifiedMind. Files: +holographic_planshape.py, holographic_unified.py, test_holographic_planshape.py, test_integration.py, tour.py, +README. + + +GRAPH-SIGNAL DENOISING (reverse-transfer RT-III1 -- mesh smoothing mapped back onto the concept graph): +the DCC reverse-transfer sweep asked which 3-D operation is a special case of a general operation the stack +LACKS. Mesh smoothing (filter a signal -- vertex positions -- on the mesh graph) is one: holostuff is full of +graphs (the codebook similarity graph, the HoloForest, the store adjacency, the scene/sequence chains), and +`graph_memory` only does cosine k-means CLUSTERING, never a Laplacian or spectral FILTER. New module +holographic_graphsignal.py is that filter -- denoise/regularize a set of vectors over its own k-NN similarity +graph (non-local means on the concept graph). + +THE TAUBIN POINT. A naive graph-Laplacian smooth denoises but SHRINKS: every step pulls mass toward the graph +mean (its transfer (1-lam*k)^n < 1 for every graph frequency k>0, DC included), so the whole codebook collapses. +Taubin's lam|mu pair (Taubin 1995) alternates a shrink step (lam>0) with an un-shrink step (mu<0, |mu|>lam) so +the combined transfer (1-lam*k)(1-mu*k) is ~1 at low frequency (DC preserved -> no shrink) and <1 at high +frequency (noise removed) -- the classic no-shrink low-pass. + +MEASURED ON A CURVED HIGH-RANK MANIFOLD (the regime where a LOCAL graph beats a GLOBAL-linear method), with the +kept negative. (1) Taubin robustly AVOIDS the shrink -- mean norm stays ~0.88-0.98 (toward the clean norm as +noise rises) where the naive Laplacian always collapses to ~0.41-0.54. Unambiguous. (2) Graph filtering BEATS +per-vector denoising (consolidation onto the global low-rank subspace) ONLY at HIGH noise: rel-noise 1.2 -> +Taubin quality 0.865 vs consolidate 0.837, winning 6/6 seeds; moderate noise ties; LOW noise (rel-0.5) -> +consolidation wins 0/6 (0.968 vs 0.953) and the graph filter OVER-SMOOTHS. KEPT NEGATIVE: the local k-NN graph +helps precisely when noise is high enough to corrupt the global linear subspace while the curved manifold's +local neighbourhoods survive; when the signal is already clean, the global linear denoiser is better and the +graph filter only blurs it. The decode-vs-evaluate cousin: the graph filter pays when the structure is genuinely +non-linear/local, not when a few global components already capture it. + +The doc's flagged failure mode (building the k-NN graph is O(n^2)) is handled by REUSING the HoloForest's +sub-linear `recall_k` for the neighbours (its own docstring already names it "the neighbour-search step that +non-local-means denoising needs"; `graph_denoise(..., sublinear=True)`), and by holding the graph as sparse +neighbour lists so the filter step is O(n*k), not a dense n*n matvec. Mind faculty: graph_denoise(vectors, k, +method='taubin'|'laplacian', sublinear). PANEL SEATS: Milanfar ("A Tour of Modern Image Filtering" links +denoisers to graph Laplacians) + Taubin (the no-shrink lam|mu surface filter). + +This is RT-III1, the panel's #1 reverse-transfer pick (cleanest bar, reuses the HoloForest). The full DCC +reverse-transfer backlog (11 items, Groups I-VI) is captured as Part II of holostuff_crosscutting_backlog.md; +the remaining ◆ picks in order are RT-II1 (nonlinear manifold chart), RT-IV1 (steering/anisotropic kernel), and +RT-I1 (operator-limit / spectral-iteration). + +Tests: +6 (1078 -> 1084). test_holographic_graphsignal.py: the module self-test, Taubin denoises + avoids the +shrink (norm kept where naive collapses), graph beats per-vector at high noise across seeds, the low-noise kept +negative (per-vector wins), and knn_graph determinism + row-normalisation. test_integration.py: the graph_denoise +faculty beating per-vector at high noise through the mind. Files: holographic_graphsignal.py, +holographic_unified.py, test_holographic_graphsignal.py, test_integration.py, tour.py, README, +holostuff_crosscutting_backlog.md. + + +NONLINEAR MANIFOLD CHART (reverse-transfer RT-II1 -- UV unwrapping mapped back onto the concept manifold): +the second DCC reverse-transfer pick. UV unwrapping (LSCM/ARAP/Tutte) is the least-holostuff item on the +backlog and secretly the most general -- distortion-minimizing FLATTENING of a curved 2-manifold to a low-D +chart, the embedding problem the whole stack faces and only solved LINEARLY by `consolidation` (an SVD). A +LINEAR projection FOLDS a curved manifold: points far apart ALONG the manifold land on top of each other in the +2-D chart. New module holographic_chart.py is the nonlinear extension. + +TWO METHODS, both pure NumPy, both reuse RT-III1's k-NN graph. (1) ISOMAP (Tenenbaum-de Silva-Langford 2000) -- +the primary, geodesic-PRESERVING chart: approximate along-manifold distance by shortest paths on the k-NN graph +(Floyd-Warshall), then classical-MDS to 2-D; this UNROLLS the curve. (2) LAPLACIAN EIGENMAPS (Belkin-Niyogi +2003) -- the graph-spectral cousin: the bottom non-trivial eigenvectors of the SAME graph Laplacian whose +high-frequency components RT-III1's Taubin filter REMOVES (so the two reverse-transfer items are one operator +used two ways). It preserves LOCAL neighbourhood structure but distorts GLOBAL distances -- kept as the honest +secondary, not the default. + +MEASURED on a swiss roll lifted into D=256 (the canonical curved 2-manifold whose ambient variance defeats a +linear projection). Isomap BEATS linear SVD/consolidation robustly: geodesic-distance correlation ~0.83 vs +~0.76 and class separation (4 bands adjacent on the manifold but FOLDED by SVD) ~0.86 vs ~0.76, winning 5/5 +seeds on both (on a clean roll the geo-corr gap is wider, 0.95 vs 0.52). Laplacian Eigenmaps preserves local +neighbourhoods but its global geo-corr trails SVD here -- the kept nuance. + +FAILURE MODE the doc flagged (honest, not a bug): a chart assumes disk topology (genus 0). A CLOSED manifold (a +torus, genus 1) cannot flatten to a plane without a SEAM -- cut it first, and the `topology` faculty finds the +genus that says where. A 1-manifold ring charts to a circle with no cut; a genus>0 surface needs the cut. High +curvature also makes some distortion unavoidable (LSCM's own limit). The geodesic step is Floyd-Warshall O(N^3) +-- fine for a few hundred points; subsample to landmarks or reuse the HoloForest neighbours (RT-III1's O(N^2) +graph-build fix) for more. Determinism: the eigenvector sign is pinned (largest-|entry| made positive) so the +chart is bit-stable -- the sign/order tie the determinism fence warns about. Mind faculty: +manifold_chart(vectors, dim, method='isomap'|'spectral', k, sublinear). PANEL SEATS: Olshausen (representation +geometry) + the consolidation thread + Tutte (graph drawing) + Lévy/Liu (LSCM/ARAP). + +Two of the four DCC reverse-transfer ◆ items now shipped (RT-III1 graph-Laplacian, RT-II1 manifold chart). The +remaining ◆ picks in order: RT-IV1 (steering/anisotropic kernel), RT-I1 (operator-limit / spectral-iteration). +Full backlog: Part II of holostuff_crosscutting_backlog.md. + +Tests: +7 (1084 -> 1091). test_holographic_chart.py: the module self-test, Isomap beats SVD on geodesic +fidelity across seeds, Isomap separates classes the linear chart folds, the chart is deterministic, the spectral +method runs, and geodesics stay finite when the raw k-NN graph starts disconnected (connectivity repair). +test_integration.py: the manifold_chart faculty beating linear SVD on a curved manifold through the mind. Files: +holographic_chart.py, holographic_unified.py, test_holographic_chart.py, test_integration.py, tour.py, README, +holostuff_crosscutting_backlog.md. + + +THE DETERMINISM CONTRACT (ISA-1 -- the first item of the VSA ISA backlog, Part III of the cross-cutting +backlog): the "learn from assembly" lens found that holostuff has ALREADY built a VSA instruction-set +architecture (the kernel is the instruction set; HoloMachine the assembler+interpreter; StructureRecipe the IR; +the resonator the disassembler), and an ISA is durable only if the EXACT OBSERVABLE semantics of its base +instructions are a frozen contract while implementations vary underneath. The audit found the cost of NOT having +that contract, paid right now: the determinism/tie-break behaviour was specified FOUR different ways -- +cleanup's implicit numpy argmax (ties->lowest index, written nowhere), spectral's "largest-magnitude component +positive" (explicitly citing "the same bit-exact-tie class as the bind_batch bug"), flow's private weighted +Laplacian, and -- the fourth, added two builds ago during RT-II1 -- chart's private `_fix_signs` reinventing the +same sign rule. Same bug class, re-litigated four times, code duplication as the price. + +WHAT SHIPPED. (1) ISA.md -- the written contract: per-instruction observable semantics (bind/unbind/bundle/ +permute/cosine/involution/random_vector + the cleanup decision), each tagged EXACT (a decision / exact reindex, +pinned bit-for-bit) or TOL (a continuous value, conformant within numeric tolerance), with the real edge cases +grounded from the kernel (zero-sum bundle -> zero vector; zero-norm cosine -> 0.0; permute exact+invertible; +involution exactly self-inverse). The ARCHITECTURE/MICROARCHITECTURE boundary stated explicitly: the observable +DECISION is architecture (pinned); how the continuous numbers are computed (FFT vs direct, batched vs looped) is +microarchitecture (free within tolerance, provided the decision it feeds is unchanged) -- bind_batch is exactly +such a variant, which is why it could be bit-exact to 1e-12 yet flip a trajectory through an unpinned argmax +tie. (2) holographic_determinism.py -- the executable embodiment of the ONE determinism rule: `fix_eigvec_signs` +(the reconciled sign convention) and `argmax_tiebreak` (names cleanup's lowest-index convention so it is +citable). (3) THE DE-SILO: spectral.sign_fix and chart._fix_signs are now thin delegates to the shared utility, +BIT-EXACT (copy=False preserves spectral's in-place behaviour; the 19 spectral/chart tests still pass unchanged) +-- the fourth scattered copy is gone, the convention has one home. + +THE ANTICIPATED NEGATIVE, kept: a contract must not over-specify. ISA.md freezes only the OBSERVABLE semantics +callers depend on (the argmax decision, unbind's approximate-recovery guarantee, the edge-case returns), NOT the +FFT's internal rounding, the bits of a reduction no decision observes, or the basis within a degenerate +eigenspace -- pinning those would mistake incidental float behaviour for architecture and block the very +optimization (the bind_batch speed-up) the contract exists to make safe. SEATS: Cranmer (reproducible-analysis / +frozen re-runnable contract, RECAST) + Macklin (the bit-exact tie-break lesson; this also answers his standing +determinism-audit request). NEXT in the spine: ISA-2 (the conformance suite + reference implementations + a +regression for the bind_batch class itself -- the contract's teeth), then ISA-3 (the extension discipline). + +Tests: +7 (1091 -> 1098). test_holographic_determinism.py: the sign rule is deterministic / sign-invariant +(V and -V -> the same fixed basis) / idempotent, the copy flag preserves each call site's behaviour, the argmax +tie-break picks the lowest index, and the de-silo is bit-exact (spectral.sign_fix and chart._fix_signs now equal +the shared rule). Files: holographic_determinism.py, ISA.md, holographic_spectral.py, holographic_chart.py, +test_holographic_determinism.py, tour.py, README, holostuff_crosscutting_backlog.md. + + +THE CONFORMANCE SUITE (ISA-2 -- the second item of the VSA ISA spine; the teeth for ISA.md): a contract with no +enforcement is just prose, so this is the enforcement. For each base instruction there is now a DEFINITIONAL +reference implementation (holographic_reference.py) -- the simplest, obviously-correct version: `ref_bind` is a +direct O(D^2) circular convolution (NOT an FFT), `ref_involution` the explicit reversal, `ref_permute` the +explicit index roll -- verified against the production kernel to MACHINE EPSILON (bind vs direct conv: 1e-16; +involution/permute: exact). The FFT `bind` genuinely IS circular convolution, now provable by a slow reference. + +THE TOL/EXACT SPLIT, made callable (the ISA-1 boundary enforced). `value_conformant` (continuous outputs match +within numeric tolerance) for bind/unbind/bundle/cosine; `exact_conformant` (bit-for-bit) for involution/permute; +`decision_conformant` (same cleanup pick under argmax_tiebreak) for the observable decision. `run_conformance` +checks every production op against its reference and returns {op: passed/class/max_diff}; exposed as the mind +faculty `conformance_report()` (the conformance harness made first-class, beside calibration_report). A +vectorized op is "conformant" iff it passes here -- which is exactly what makes the §7 vectorization sweep safe +to pursue. + +THE CENTERPIECE -- the bind_batch-class regression, caught BY CONSTRUCTION. The bug was a value-conformant +change (bit-exact to 1e-12) that flipped a creature's trajectory through an unpinned argmax tie. The suite +catches the whole class because it checks the DECISION separately and exactly: a similarity vector perturbed by +a SUB-TOLERANCE amount (1e-12) passes `value_conformant` but fails `decision_conformant` -- a value-only suite +would accept it, the contract's decision check rejects it. And the literal mechanism is pinned too: the same +numbers summed in two orders (x = [1e16, 1, -1e16, -1]) give -1.0 vs 0.0, and on a near-tie that flips the +argmax. GOLDEN VECTORS are the hand-verifiable convolution identities (bind(a, delta0)==a; bind(a, delta_k)== +roll(a,k); bind commutative; the round-trip recovers b as the cleanup WINNER -- approximate because involution +is an exact inverse only for unitary vectors, not random ones, so the guarantee is the decision, not a 1e-9 +match) -- goldens that cannot rot the way frozen float arrays would. EDGE CASES pinned: zero-sum bundle -> zero +vector; zero-norm cosine -> 0.0. + +SEATS: Cranmer (the conformance/measurement discipline; golden tests as the spec made executable). NEXT in the +spine: ISA-3 (the extension discipline -- document Clifford/tensor/FPE as named, opt-in ISA extensions, base +kernel stays minimal). NOTE (honest): the doc suggested ISA-2 lets flow's Laplacian be de-duplicated, but the +audit shows flow's `_weighted_laplacian` is a DIFFERENT construction (edge-conductance for the Tero solve) than +the k-NN similarity Laplacian in graphsignal/chart -- not a duplicate to merge, so that de-silo is not forced. + +Tests: +9 (1098 -> 1107). test_isa_conformance.py: all base ops conform to their references (TOL/EXACT), the +convolution-identity golden vectors, exact ops bit-for-bit + self-inverse/invertible, the zero-vector edges, and +the bind_batch-class regression (a value-conformant change that flips a decision is caught; a summation reorder +flips an argmax). test_integration.py: the conformance_report faculty passing for the live kernel. Files: +holographic_reference.py, ISA.md (referenced), holographic_unified.py, test_isa_conformance.py, +test_integration.py, tour.py, README, holostuff_crosscutting_backlog.md. + + +THE GOVERNED EXTENSIONS (ISA-3 -- the third item of the VSA ISA spine; closes the Tier 0-1 do-now block): real +instruction sets grow as base + extensions, never by bloating the base. The audit confirmed holostuff already +does this by instinct (the Clifford module's docstring states the rule), so ISA-3 makes it POLICY: +ISA_EXTENSIONS.md is the VSA analog of x86 + SSE/AVX/AES-NI -- a minimal base kernel plus named, opt-in bind-mode +EXTENSIONS, each justified by a MEASURED regime win over base `bind`. + +THE BASE/EXTENSION BOUNDARY (the principle, applied to the debatable `permute` case): BASE = what (almost) every +faculty uses, the holographic_ai.py kernel; EXTENSION = regime-specific, a separate opt-in module. By that rule +the base instruction set is frozen as random_vector / bind / unbind / bundle / permute / cosine / involution / +the cleanup decision (full semantics in ISA.md) -- and `permute` is BASE (it lives in the kernel, used across +the sequence/creature/structure faculties for order). The three extensions are NOT base. + +THE THREE EXTENSIONS, each with a regime win MEASURED FRESH this session: (1) Clifford-bind (the geometric +product, holographic_clifford.py) -- regime 3-D rotations; win: rotation composition is EXACT and is one product +(the geometric product of two rotors IS the composed rotor, error 1.1e-16), and non-commutative so it captures +order base convolution cannot; cost: 2^d-dimensional, rules it out as a general substrate. (2) FPE/VFA +(holographic_fpe.py) -- regime continuous/spatial values; win: a DESIGNED Bochner kernel makes nearby values +similar (smooth monotone falloff 1.0->0.9->0.66->0.41->0.22->0.11->0.04 over offset 0..3) where independent +random atoms have NO continuity (all ~0 off-diagonal); cost: it is an encoder, not a general bind, and the +kernel is a design choice. (3) Tensor-product bind (holographic_tensor.py) -- regime high capacity at the cost +of D^2 storage; win: at a load that overloads HRR, tensor recall 0.87 vs HRR 0.28 (D=32, 12 pairs); cost: D^2 +numbers vs D, and a generic full-rank binding cannot be MPS-compressed without losing recall (the frontier is +HRR(D) < tensor-train(~2rD) < full tensor(D^2)). + +THE NEW-EXTENSION PROPOSAL TEMPLATE (the earning-its-place bar, baked into the doc): name & module (base +untouched -- conformance_report still passes); regime (and where NOT to use it); the base-bind baseline it must +beat; the MEASURED win on real data with the regime stated; its own conformance test; cost & kept negative +stated as loudly as the win. No measured regime win -> not an extension; the base stays minimal. SEATS: +Stoudenmire (the tensor/capacity extension) + Plate (what stays in the minimal base ISA). The honesty/anticipated +negative (the boundary is debatable) is resolved by picking the principle and applying it consistently rather +than case-by-case. + +Tiers 0-1 of the ISA spine are now complete (ISA-1 contract, ISA-2 conformance teeth, ISA-3 extension +discipline) -- the do-now block that makes the whole engine safer to optimize and systematizes a design +holostuff already followed by instinct. NEXT: ISA-4 (accumulator -> a small register file; register pressure is +literally a capacity-cliff question), the first machine-model item. + +Tests: +4 (1107 -> 1111). test_isa_extensions.py: Clifford exact rotation composition (length-preserving + +invertible), FPE's designed kernel is continuous and beats random atoms, tensor-bind's higher recall at an +overloading load, and the base kernel stays minimal/unchanged when the extensions are imported. Files: +ISA_EXTENSIONS.md, test_isa_extensions.py, tour.py, README, holostuff_crosscutting_backlog.md. + + +THE REGISTER FILE (ISA-4 -- the first machine-model item of the ISA spine): HoloMachine ran everything through +ONE accumulator (ACC). ISA-4 grows it to a handful of named slots (REGISTERS R0..R7) with two new opcodes, +STORE r (ACC -> slot) and RECALL r (slot -> ACC). Backward-compatible: the two opcodes are additive, existing +programs are untouched (all 14 prior machine tests pass), and the operand is cleaned against a new reg_atoms +codebook exactly like REPEAT's counts or APPLY's faculty names. + +THE DESIGN HINGES ON THE KEPT NEGATIVE (the lovely VSA-native one): how is the register file held? Two options, +and the measurement decides. (A) SEPARATE NAMED SLOTS (a Python dict in run()): reads are EXACT (the value is +returned verbatim, cosine 1.000, bit-for-bit via np.array_equal), no crosstalk, no capacity limit. (B) ONE +BUNDLE (the machine's existing "disk" pattern, bundle of bind(reg_role_i, value_i) with a distinct role per +register): the slots share the crosstalk budget, so readback degrades as registers pile in -- "register pressure" +is LITERAL, the capacity cliff applied to the register file. MEASURED: a bundled file reads back perfectly to ~16 +registers at dim 1024, then degrades (32 -> 0.99, 64 -> 0.93/0.91); at dim 4096 it holds 64. So register count is +a CAPACITY QUESTION for the bundled rep, not a free choice. The machine therefore holds slots SEPARATELY -- the +measurement is the justification for the design, exactly the project's pattern (ship the working design, keep the +negative that rules out the alternative on record). + +THE BAR (registers save re-derivation): a value needed again after ACC moves on costs a full re-derivation +without registers but ONE RECALL with them. For a k-instruction intermediate M, registers replace k instructions +with 1 (plus 1 STORE) -- fewer instructions, and the recalled M is EXACT regardless of how it was produced (a +re-derivation that ran a lossy APPLY step would not even reproduce M; RECALL always does). SEATS: Plate (the +clean HRR slot/role algebra the register file is built from) + Eno (a small, well-chosen set of named slots as a +generative constraint -- a handful, not unlimited). + +NEXT in the spine: ISA-5 (a documented calling convention + a permute-stack for recursion -- and the kept +negative there is the same family: stack depth is bounded by crosstalk, like the B8 iterated-decode cliff). + +Tests: +6 (1111 -> 1117). test_isa_registers.py: exact read, bit-for-bit recall of an intermediate, the +re-derivation-instruction saving, 8 independent slots, and the bundled-file capacity-cliff kept negative; plus a +mind-level integration test (exact recall through the mind's machine + a register-free program still runs). +Files: holographic_machine.py, test_isa_registers.py, test_integration.py, tour.py, README, +holostuff_crosscutting_backlog.md. + + +THE CALLING CONVENTION + PERMUTE-STACK (ISA-5 -- the second machine-model item of the ISA spine): two parts, a +documented ABI and a substrate stack. (1) THE CALLING CONVENTION (ISA.md's new ABI section): CALL f runs library +function f as an ACC->ACC transform -- ACC is the argument in and the return value out, and the whole function +library obeys it. The preservation guarantee is the nice part: registers (R0..R7) and the permute-stack are +FRAME-LOCAL -- each CALL runs in its own run() frame with a fresh register file and a fresh stack, so a callee +CANNOT corrupt the caller's registers or stack (measured: a callee overwriting its R0 leaves the caller's R0 +bit-identical, cosine 1.000). In ABI terms every register is callee-saved by construction -- the caller spills +nothing to keep a value across a CALL. Recursion (self-CALL with an IFMATCH base case) runs under the existing +depth guard (8). + +(2) THE PERMUTE-STACK (PUSH/POP opcodes + module-level stack_push/stack_pop): a LIFO in the vector substrate. +PUSH is permute+bundle (shift the existing items one level deeper, drop ACC on top); POP is cleanup + +inverse-permute (the top is the only un-permuted term -- clean it out, peel it off, un-shift the rest). It is the +explicit-stack form of recursion -- e.g. reversing a sequence by pushing every element then popping pops them in +reverse, the textbook stack-replaces-recursion pattern -- and it runs correctly through the machine (push a,b,c,d +-> first POP yields 'd'). + +THE KEPT NEGATIVE, measured (the spine's recurring lesson, a third time): the permute-stack is a HOLOGRAPHIC +stack -- every level rides one bundle, so depth is bounded by crosstalk exactly like the B8 iterated-decode +cliff. SAFE DEPTH ~4-8 items at dim 1024 (LIFO recovery 1.00 to depth 4, ~0.92 at 8, ~0.48 by 16; a little +deeper at dim 4096). So the permute-stack is for shallow nesting of cleanup-able items; for arbitrary +intermediates at any depth, use the registers (exact, frame-local). This is the SAME capacity lesson the bundled +register file taught (ISA-4) and the bundled disk before it: superposition buys composability and pays in a +crosstalk cliff -- measure it, keep the exact path for what must be exact. SEATS: Plate (the HRR role/permute +algebra the convention and stack are built from) + the machine thread. + +NEXT in the spine (Tier 3): ISA-6 (a macro layer -- parameterized recipe/procedure templates over the assembly, +the procedure abstraction is already halfway there). Then ISA-7 (a small HLL, research) and ISA-8 +(reversible/quantum bind, the frontier). + +Tests: +6 (1117 -> 1123). test_isa_callstack.py: frame-local registers (the ABI guarantee) and frame-local stack, +the permute-stack LIFO primitive, reverse-via-stack through the machine (the bar), and the depth-cliff kept +negative; plus a mind-level integration test (reverse-via-stack + frame-local registers through the mind's +machine). Files: holographic_machine.py, ISA.md, test_isa_callstack.py, test_integration.py, tour.py, README, +holostuff_crosscutting_backlog.md. + + +THE MACRO LAYER (ISA-6 -- Tier 3, the first layer above assembly): parameterized recipe TEMPLATES, in +holographic_template.py. A template is a StructureRecipe with named HOLES filled at instantiation -- "tag a value +under a role" (pair), "a two-field record" (record), "an order-bearing pair" (ordered_pair) -- written once and +instantiated with different arguments. Because a StructureRecipe replays BIT-EXACT (atoms are regenerated from +the seed by name), instantiating with different arguments produces the correct DISTINCT structures +deterministically (the recipe's exactness carries -- the bar). A starter library (STARTER_LIBRARY) ships three; +the mind exposes `instantiate_template(name, **args)` and `template_names()`. + +THE KEPT NEGATIVE -- MACRO HYGIENE, designed out: atoms are derived from NAMES, so two atoms with the same name +(and kind) are the SAME vector. A template that creates an internal role atom named "role" would COLLIDE with a +caller who fills a hole with an atom also named "role" -- role and value become one vector (capture) and the +binding degenerates. The fresh-atom discipline: template-INTERNAL atoms are namespaced under a reserved prefix +("@tmpl::") that a caller's bare names cannot hit -- a gensym keyed by template name (so the same template +stays deterministic across instantiations). The witness of capture is cosine(internal_role, caller_value) at +matched kind: ~0 (-0.04 measured) with the discipline, 1.0 without it. `RecipeTemplate` is hygienic by +construction; `_UnhygienicTemplate` (tests only) exhibits the capture the discipline prevents. (Roles are unitary +so the single-binding `pair` recovers its value EXACTLY on unbind; the 2-field `record` recovers each field +approximately -- a 2-item bundle -- but cleanly separable, the right value winning by a wide margin.) SEATS: +Puckette (Pd/Max -- a composition language of parameterized patches over real-time primitives) + the +recipe/procedure thread. + +NEXT in the spine (Tier 3, research-heavy): ISA-7 (a small higher-level language that lowers to the recipe IR -- +the DCC material-node-graph-as-recipe and the typed-structure unification are already early forms) and ISA-8 +(reversible/quantum bind, the frontier). + +Tests: +9 (1123 -> 1132). test_holographic_template.py (8): bit-exact determinism, distinct structures from +distinct args, exact recovery for a single-binding template, separable record fields, order-sensitivity of +ordered_pair, the starter library, the hygiene/capture kept negative, and the reserved-namespace convention; +plus a mind-level integration test (instantiate_template -> distinct bit-exact + exact pair recovery through the +mind). Files: holographic_template.py, holographic_unified.py, test_holographic_template.py, test_integration.py, +tour.py, README, holostuff_crosscutting_backlog.md. + + +THE STRUCTURE LANGUAGE (ISA-7 -- the top of the assembly tower; the last buildable spine item before the +frontier): a small declarative language that LOWERS to the recipe IR, in holographic_lang.py. The surface is +S-expressions: a bare symbol is an atom; (bind a b) / (bundle ...) / (permute a n) lower to the matching recipe +ops; and the ISA-6 templates appear as language forms -- (record name moose), (pair x) -- so the macro layer +becomes language constructs. parse/unparse round-trip the surface (parse o unparse == identity); compile_spec +lowers an AST to a StructureRecipe; realize_spec materialises the vector. The mind exposes compile_structure and +realize_structure. The whole framing: the typed unification (program = tree = scene = record = one +StructureRecipe) IS the IR this language targets -- assembly (the kernel) -> macros (templates) -> language, one +tower over one substrate. + +THE BAR met: a declarative spec compiles to a CORRECT recipe and realizes BIT-EXACT -- (bind a b) realizes +exactly to bind(atom a, atom b); a (record ...) form is bit-identical to the ISA-6 template instantiated +directly (the layers agree); same spec -> same vector. And it round-trips on the surface. + +THE KEPT NEGATIVE / SCOPE BOUNDARY (heeded, not discovered): a general-purpose language is large and easy to +over-scope, so ISA-7 is SCOPED TO ONE DOMAIN -- structure description. There are NO variables, NO control flow, +NO user-defined functions: just atoms, the base binds, and the fixed template library. The scope is enforced, not +aspirational: an unknown form (while ...) is a ValueError, not a silent no-op, and template/base arities are +checked. This is the "do not build a general language up front" discipline made into test_scope_boundary. SEATS: +Puckette (a declarative DSP language over primitives -- Pd is exactly "a surface that lowers to a small set of +real-time ops") + Eno (language-as-generative-system) + Plate (the IR). + +NEXT in the spine -- the frontier (Tier 4, research): ISA-8 (the reversible-computing / error-correction model -- +cleanup AS error correction, unbind as the exact inverse of bind). That is the last spine item, and the most +speculative. + +Tests: +9 (1132 -> 1141). test_holographic_lang.py (8): surface round-trip, correct lowering of the base forms, +bit-exact determinism, template-forms-agree-with-ISA-6, nested composition recovery, compile-returns-a-replayable +-recipe, the scope-boundary kept negative (unknown form / bad arity are errors), and parser rejection of +malformed input; plus a mind-level integration test (realize_structure + compile_structure + agreement with +instantiate_template through the mind). Files: holographic_lang.py, holographic_unified.py, +test_holographic_lang.py, test_integration.py, tour.py, README, holostuff_crosscutting_backlog.md. + + +THE REVERSIBLE / ERROR-CORRECTION MODEL (ISA-8 -- the frontier, the LAST item of the VSA ISA spine): names what +the engine has been all along and ships one measured payoff. In holographic_reversible.py + ISA_REVERSIBLE.md. +THREE parts, honestly labelled by what they are. (a) THE REVERSIBILITY AUDIT (framing, but testable): +bind/unbind/permute/involution are REVERSIBLE (exact inverse -- verified: unbind o bind == identity for a unitary +key, permute by -shift, involution self-inverse); bundle/superpose/cleanup are INFORMATION-DESTROYING (sum or +projection -- no exact inverse). The organizing read: the lossy ops are where the coherence budget is spent, and +CLEANUP IS ERROR CORRECTION (snap to the codebook manifold, discard the accumulated error). (b) THE AUTO-CLEANUP +SCHEDULER (THE PRACTICAL CORE, measured): a long program accumulates crosstalk and drifts toward a cliff where +cleanup would snap to the WRONG atom; the scheduler inserts cleanup BEFORE the cliff using an ORACLE-FREE health +signal -- cosine of the running vector to its nearest atom (1.0 on a clean atom, falling as it drifts; the +capacity diagnostic's SNR proxy). 'adaptive' cleans only when health < floor; 'fixed' cleans every k. This +generalizes the shipped coherence-gate from store-MAINTENANCE to program-EXECUTION. MEASURED (bursty damage): +adaptive holds the output above a 0.9 fidelity threshold (frac-below 0.000) at 5 CLEANUPS; the best fixed cadence +that matches that fidelity (k=3) needs 16 -- ~1/3, echoing the coherence-gate's "matched accuracy at ~1/3 the +passes." Fixed cadences using fewer (k=4->12, k=6->8) drop below threshold. (c) THE QUANTUM-GATE CONNECTION +(framing only): FHRR's bind is a diagonal unitary (per-frequency phase rotation), structurally gate-like, which is +why unitarity makes bind exactly invertible -- useful for capacity-as-coherence-budget reasoning, nothing more. + +THE LOUD NEGATIVE (the most important honesty note on the spine): this is an ANALOGY, NOT physics. VSA is NOT a +quantum computer -- no exponential superposition, no entanglement, no quantum speedup. We borrow the DISCIPLINE +(error budget + correct-before-the-cliff + reversibility bookkeeping); we do not overclaim the physics. The +practical do-able core is the scheduler (b); (a) and (c) are scaffolding. KEPT honest within (b): under CONSTANT +damage a fixed cadence is already near-optimal, so the adaptive win is specific to VARIABLE damage rates (when +the right fixed k cannot be known in advance); and the health floor must trigger early enough that the nearest +atom is still the true one when cleanup fires. SEATS: Stoudenmire (quantum-inspired/tensor networks; the +FHRR-as-diagonal-unitary framing) + the FHRR/honesty/coherence threads. + +*** THE VSA ISA SPINE IS COMPLETE: ISA-1 (determinism contract) -> ISA-2 (conformance suite) -> ISA-3 (extension +discipline) -> ISA-4 (register file) -> ISA-5 (calling convention + permute-stack) -> ISA-6 (macros) -> ISA-7 +(structure language) -> ISA-8 (reversible/error-correction model). Eight items, each with its kept negative on +record; the recurring lesson across the whole spine -- superposition buys composability and pays in a crosstalk +cliff, so measure the budget and keep an exact path -- appeared as the bundled disk, the bundled register file, +the permute-stack depth cliff, and finally as the coherence budget the auto-cleanup scheduler manages. *** + +Tests: +7 (1141 -> 1148). test_holographic_reversible.py (6): the audit classification (+ unknown-op error), +reversible ops actually round-trip, lossy ops destroy information (bundle mixes, cleanup is idempotent), the +health signal tracks drift, the adaptive-beats-fixed scheduler bar (~5 vs ~16 cleanups at matched fidelity), and +the no-cleanup degradation control; plus a mind-level integration test (reversibility_audit + run_with_auto_cleanup +through the mind). Files: holographic_reversible.py, ISA_REVERSIBLE.md, holographic_unified.py, +test_holographic_reversible.py, test_integration.py, tour.py, README, holostuff_crosscutting_backlog.md. + + +ANISOTROPIC / STEERING KERNELS (RT-IV1 -- the DCC reverse-transfer item deferred while the ISA spine was built; +now picked up): a direction-dependent metric for the FPE encoder. holographic_fpe.py's VectorFunctionEncoder now +accepts a PER-AXIS bandwidth (a list, one per axis) as well as a scalar -- a diagonal anisotropic kernel: SMALL +bandwidth on an axis = a wide, smooth kernel there; LARGE bandwidth = a sharp one. This is the bounded form of +Milanfar's steering kernel (Takeda/Farsiu/Milanfar 2007) -- n bandwidths, not a per-point covariance that +overfits -- and it is the same object as an anisotropic Gaussian splat (per-splat covariance), the cross- +connection Drettakis' seat noted. Backward-compatible: a scalar bandwidth broadcasts to all axes (the original +isotropic behaviour). holographic_steering.py adds steer_bandwidths (fit per-axis bandwidths from the data's +directional smoothness -- sharp axis -> large bandwidth) and kernel_regress (FPE-kernel-weighted Nadaraya-Watson), +plus the mind faculty steering_regress. + +THE BAR met, in the RIGHT REGIME (this took several honest iterations to locate): on DENSE, strongly-directional +data -- a sharp ridge/edge, constant along one axis and sharp across another -- the steered anisotropic kernel +beats the best isotropic RBF by ~8% (grid-vs-grid), pooling the many same-value samples ALONG the flat direction +while staying sharp across the edge. This is the regime steering kernels are actually designed for (image edges, +dense samples). + +THE KEPT NEGATIVES (loud, because anisotropy is easy to oversell -- several iterations of the prototype kept +failing until the regime was right): (1) On SPARSE scattered data the advantage collapses to ~1-3% -- not enough +samples to pool along the flat direction; isotropic stays the honest baseline. (2) On ISOTROPIC data (equal +structure both axes) anisotropy gives ~0%, as it must not. (3) The framing "low frequency = can pool widely" is +WRONG when the low-frequency axis still spans a full period over the domain -- there must be a genuine +low-VARIATION direction, not merely low frequency. (4) The STEERING ESTIMATE is unreliable on scattered data: a +per-axis gradient estimated from scattered points is polluted by the OTHER axes varying and can point the WRONG +way (an early prototype steered backwards) -- it needs dense/grid sampling (neighbours that differ in just one +axis) to estimate cleanly, and a perfectly-flat axis gives gradient 0 (guarded against nan). A full per-point +covariance is worse still (the splat module's own anisotropy negative). So: diagonal bandwidths, dense +directional data, isotropic as the fallback. SEATS: Milanfar (steering-kernel regression) + Drettakis +(anisotropic splats). + +NEXT (the reverse-transfer thread): RT-I1 (operator_limit / spectral-iteration -- the subdivision = dynamics = +diffusion = resonator unification, the most beautiful but most O(n^3)-haunted; do it in the Fourier/structured +form). The broader BACKLOG.md items (external-baseline harness, theory-and-guarantees doc) also remain. + +Tests: +8 (1148 -> 1156). test_holographic_steering.py (7): FPE per-axis bandwidth is anisotropic, scalar +bandwidth is backward-compatible, per-axis length is checked, the anisotropic-beats-isotropic dense-ridge bar, +steering recovers the right direction on dense data, steering handles a perfectly-flat axis (no nan), and the +isotropic-data no-advantage kept negative; plus a mind-level integration test (steering_regress beats a matched +isotropic baseline on the dense ridge through the mind). Files: holographic_fpe.py, holographic_steering.py, +holographic_unified.py, test_holographic_steering.py, test_integration.py, tour.py, README, +holostuff_crosscutting_backlog.md. + + +SPECTRAL ITERATION (RT-I1 -- the last and most conceptual DCC reverse-transfer item; the unification the backlog +called "the most beautiful but most O(n^3)-haunted"): diagonalise an iterated bind operator once, evaluate any +level or the limit in closed form. In holographic_iterate.py. THE KEY INSIGHT that dissolves the O(n^3) worry: a +bind is circular convolution, which is DIAGONAL in the Fourier basis -- so the eigenvalues of the bind operator U +are simply its rfft spectrum and the eigenvectors are the Fourier modes. The eigendecomposition is FREE (it is +the FFT), never a dense SVD at D=4096 (exactly what the topology module timed out on). This is "live in the +Fourier/structured form where the spectrum is free." + +The unification the backlog pointed at: subdivision (Stam's exact eval), the dynamics propagator's k-step rollout +(learn_dynamics), the diffusion sampler's steady state (hopfield.generate), and the resonator's fixed points are +ALL "iterate a linear operator." Given U: (1) the k-step iterate is ONE eval -- raise the transfer to the k-th +power -- matching k sequential binds to FFT tolerance (~1e-15, MEASURED: a 20-step jump == 20 binds to 9e-16); +(2) the limit is closed-form -- decaying modes (|eigenvalue|<1) vanish, persistent modes (|.|~1) remain, a +contractive operator's limit is 0 with NO iteration; (3) convergence/stall is READ OFF the spectrum before +running -- the regime from max|eigenvalue| (contractive -> decays, marginal -> persists, divergent -> blows up), +and the power-iteration rate from the spectral gap |lambda_2|/|lambda_1| (small gap -> slow / near-degenerate +stall). Mind faculties propagator_jump (one-eval k-step) and propagator_spectrum (regime + gap, without running). + +THE KEPT NEGATIVES: only LINEAR operators diagonalise this way; the TRUE resonator is nonlinear (alternating +projection + cleanup) and needs delay-embedding -- the spectral prediction is exact for the linear iterate (the +dynamics propagator, power iteration) and only a HEURISTIC for the nonlinear resonator (the dynamics module's own +nonlinearity negative). So the clean exact results are the linear iterate; the "predict a resonator stall from the +spectrum" bar is met in its linear cousin (power-iteration convergence from the spectral gap), with the nonlinear +caveat on the record. Eigenvector sign is pinned (largest-|entry| positive) for determinism -- the ISA-1 fence. +SEATS: Stam (exact subdivision eval = an eigendecomposition of the refinement matrix) + Stoudenmire (spectral/ +low-rank) + Koopman/DMD. + +*** THE DCC REVERSE-TRANSFER THREAD IS COMPLETE: RT-III1 (graph-Laplacian denoise) -> RT-II1 (nonlinear manifold +chart) -> RT-IV1 (steering kernels) -> RT-I1 (spectral iteration). Four reverse-transfers from the 3D/DCC domain +into the engine, each measured with its kept negative; the reverse-transfer paid (the engine gained a graph +filter, a curved-manifold chart, a direction-dependent metric, and a free closed-form operator iterate). *** + +Tests: +8 (1156 -> 1164). test_holographic_iterate.py (7): the eigendecomposition is the free rfft, the k-step +jump matches the k-bind rollout, the contractive limit is closed-form zero, a divergent operator has no finite +limit, the regime is read off the spectrum before running, the spectral gap predicts power-iteration speed, and +the dominant eigenvector is deterministic + unit + the power-iteration fixed direction; plus a mind-level +integration test (propagator_jump matches the learned propagator's rollout + propagator_spectrum reads a regime). +Files: holographic_iterate.py, holographic_unified.py, test_holographic_iterate.py, test_integration.py, tour.py, +README, holostuff_crosscutting_backlog.md. + + +================================================================================ +FORWARD DCC -- the explicit polygon-geometry thread BEGINS (FWD-1 + FWD-2, the Step-0 vertical slice). + +THE GAP THIS OPENS: holostuff's geometry has always been IMPLICIT/native -- an SDF is a function (field), a +splat scene is a bundle (splat), a scene-graph is recursive bind/bundle. All mature, all measured. But the +EXPLICIT side -- an actual indexed polygon mesh of the kind Blender/three.js/glTF speak -- did not exist: +grep confirmed no half-edge, no marching cubes, no gltf/glb, no Mesh class anywhere in 133 modules. The Forward +DCC backlog's Tier 0 is the GATE: stand up a conformant mesh kernel (FWD-1) and the binary boundary to a +three.js front end (FWD-2), and prove the whole boundary end-to-end FIRST as a minimal vertical slice before +building the toolkit on top. That slice is this entry. + +FWD-1 -- holographic_mesh.py (the mesh kernel). A Mesh is vertices (V,3 float) + faces (tuples; tris, quads, +n-gons all allowed) + optional normals/uvs/colours. The half-edge adjacency (half_edges() -> origin/face/nxt/ +twin arrays, cached, deterministic) is the load-bearing structure: it makes neighbour/face/one-ring queries +O(local) instead of O(scan), and it REJECTS non-manifold input loudly (the same directed edge twice -> ValueError) +rather than silently building a corrupt topology. On TOP of it: euler_characteristic / is_closed / is_manifold / +genus (the closed cube reads V8 E12 F6 chi=2 g=0, MEASURED in the selftest), vertex_normals (Newell's method, +area-weighted), triangulate (fan, convex only -- the honest scope limit), to_buffers/from_buffers (flat indexed +float32 position/normal/uv + a triangle index buffer, the GPU-ready form), to_obj/from_obj (topology-preserving +round-trip). Primitives box / tetrahedron / grid for tests and demos. + +THE KEPT NEGATIVE (loud, in the module docstring): NumPy is the WRONG tool for tight per-element mesh-edit loops. +Half-edge traversal and incremental edits (split/collapse/flip) are pointer-chasing, not vectorizable; the +selftest prints the build rate (~1.5M half-edges/s) as evidence that this is Python-loop bound. So this kernel is +correct, deterministic, and fine for the geometry SIZES the engine actually manipulates -- but it will NOT scale +to interactive million-poly editing without a compiled core. "NumPy-only" is the ENGINE's rule, not an +interactive-mesh-editor's rule, and pretending otherwise would be the kind of unmeasured claim this project exists +to avoid. The vectorized paths (Euler, normals, buffer build) are fast; the edit loops are the ceiling. + +FWD-2 -- holographic_gltf.py (the three.js boundary). mesh_to_glb(mesh) -> bytes: a real glTF 2.0 binary +container (12-byte header + JSON chunk + BIN chunk), POSITION/NORMAL/TEXCOORD_0/COLOR_0 accessors + a triangle +index buffer, the REQUIRED POSITION min/max bounds, a default PBR material, uint16 indices for small meshes else +uint32, little-endian throughout, sort_keys on the JSON so the output is BYTE-REPRODUCIBLE (the determinism rule +reaching all the way to the wire format). glb_to_mesh parses it back; validate_glb returns a structural-conformance +dict (magic / version / chunk lengths / chunk order / 4-byte alignment / position bounds). The cube emits a +1300-byte .glb that round-trips positions/normals/uvs and is structurally valid -- MEASURED in the selftest. + +THE INDEPENDENT CHECK: the .glb was loaded with the real third-party pygltflib OFFLINE (scratch only, NOT added to +the suite -- it's a banned dependency, used once as an external oracle exactly the way an external baseline should +be): it loaded cleanly, reported 8 vertices, correct min/max. So "three.js-loadable" is not a hope, it's verified +against an independent glTF reader. The delta/patch channel (ARCH-2, send only what changed) is explicitly +DEFERRED with a loud note in the module -- it is the backlog's highest-value architectural addition and earns its +own thread, not a rushed corner of this one. + +WIRED AS FACULTIES (the close-out ritual, additive + backward-compatible): mesh_box / mesh_tetrahedron / +mesh_grid / mesh_euler / mesh_to_gltf / mesh_from_gltf on UnifiedMind, inserted before the SEARCH & DYNAMICS +section. These are explicit-geometry I/O, NOT VSA hypervector ops -- the docstrings say so plainly; the bridge that +makes a mesh a hypervector (mesh <-> SDF <-> splat) is FWD-11/ARCH work, deliberately not faked here. + +SEATS: Drettakis (the glTF/three.js boundary and the splat<->mesh bridge to come) + Pharr (indexed buffers and the +acceleration structures meshes feed) + Macklin (the half-edge as the substrate a constraint/edit solver would ride, +and the determinism discipline on connectivity). Connectivity is naturally EXACT (integer indices, no float drift); +normals are TOL (continuous, feeding no decision) -- the ISA-1 fence applied to the new layer. + +A NON-OURS NEGATIVE SURFACED BY THE FULL REGRESSION (recorded honestly, not absorbed silently): the full suite run +turned up that test_holographic_market.py::test_big_dai_structure_holds_at_scale is FLAKY on the UNTOUCHED upload +(fails ~1 run in 3). Root cause pinned: it is hash-seed dependent -- with PYTHONHASHSEED fixed it passes 3/3 every +time. The market return-SIGN sequence is genuinely near-random (the efficient-market verdict the test asserts), so +it sits right at the z<2.0 boundary, and hash-seed-dependent set/dict iteration order in the bundling chain +occasionally nudges the order-sensitive float sum across the line. This is a real (minor) determinism-contract gap +-- the contract pins RNG seeds but not hash-seed iteration order -- and it is ORTHOGONAL to the mesh slice (the +slice touches none of that code). Fix is a separate task (pin PYTHONHASHSEED in conftest, OR canonicalize the +bundling order, OR widen the test's statistical band). Left on the record, not papered over. + +THE STRATEGIC FORK STILL OWED TO THE OWNER (the backlog's own "decide before FWD-1"): mesh-first (chase Blender +parity) vs native-first (play to the SHIPPED SDF/splat strengths and reach usefulness sooner). The slice plus all +of Tier 1 (UV/smoothing/geodesics/curvature, ported from the already-shipped chart/graphsignal/steering modules) +are valuable under EITHER answer -- so building the slice DE-RISKED the decision rather than pre-empting it. The +ordering of everything below FWD-2 waits on that call. + +Tests: +30 (1164 -> 1194). test_holographic_mesh.py (15): Euler invariants on box/tetra/grid, half-edge +reciprocity + cycle closure, neighbour/face queries, outward + unit normals, buffer + OBJ round-trips, the OBJ +slash-face form, non-manifold rejection, degenerate-face rejection, deterministic index buffer, sorted edges. +test_holographic_gltf.py (11): structural validity, 4-byte alignment, position/normal/uv round-trips, triangle +count, position bounds, BYTE-reproducibility, tetra+grid meshes, uint16 index path, bad-magic rejection, file +round-trip. test_integration.py (+4): the mind exposes the mesh faculties, the Euler invariant holds THROUGH the +mind, the cube->glb->cube boundary round-trips through the mind (THE vertical slice), and that boundary is +byte-reproducible through the mind. Files: holographic_mesh.py, holographic_gltf.py, holographic_unified.py, +test_holographic_mesh.py, test_holographic_gltf.py, test_integration.py, tour.py, README, NOTES_concepts.md. + + +================================================================================ +CONSOLIDATION -- the chunkers/tilers/stores converge onto ONE routing fabric (StructuredIndex keying + +TiledStore). The capacity-cliff cure ("route each item to a bounded-load chunk") had been re-grown five +times -- splat tiles, chunk_route, the instruction chunker, the RP-tree forest, the FacetStore buckets -- +each module's docstring even NAMING the others ("the same trade chunk_route makes"). This collapses the +duplication onto the shared primitive that already existed (StructuredIndex), the de-siloing the integration +plan flagged. + +THE ORGANISING LAW (recovered from the RAM/addressing thread, "as above so below"): the capacity cliff is a +property of ONE bundle, not of the problem; you escape it HORIZONTALLY (RAID-style, capacity = K x per-vector +budget, the HoloArray) and you ADDRESS the shards by a PIVOT -- and the pivot you pick IS the regime. Hash of +the key -> the page-table / LBA / DHT regime: deterministic routing with ZERO comparisons (this is "RAM" -- +you COMPUTE where it is, you do not search). Random projection -> nearest-neighbour content recall. Floor- +divide a coordinate -> spatial tiles. One fabric, one parameter. + +WHAT SHIPPED (additive, backward-compatible): + * StructuredIndex gains `keying=` (holographic_tree.py). 'projection' (default) is the original RP-tree + content recall, BYTE-FOR-BYTE (the 15 existing tree tests are the parity net, green). 'hash' is the RAM / + page-table regime -- a blake2b address (NOT Python's salted hash, which would reshuffle buckets every + process and break the determinism rule), ~O(1), exact, with absent keys returning None. 'spatial' floor- + divides a coordinate into a tile. MEASURED on the substrate before writing it: at N=5000, hash routes in + 1.04 comparisons and spatial in 2.01 (vs 5000 for a flat scan) while projection takes ~594 -- the RAM + thread's law confirmed (computed-address routing is zero-comparison; NN routing is sublinear-not-zero). + * `_tile_bucket(coord, tile)` -- the floor-divide route, now in ONE place; the spatial index, TiledStore, + AND the splat tiler all call it instead of each re-deriving `gy // tile, gx // tile`. + * `TiledStore` -- the splat-tiler's core, generalised. The KEY DESIGN INSIGHT the audit forced: the clients + vary on TWO axes, not one. ROUTING (how key -> bucket: projection / hash / spatial) is shared. STORAGE + (what a bucket HOLDS: explicit keys you FIND, vs a bounded bundle you DECODE) is the one thing that + differs -- which is why TiledStore is a SIBLING class, not a flag on the index. Bundling is FORBIDDEN in + an index (rule 2: a superposed index caps with set size) yet CORRECT in a bounded-load tile (the decode + cap never bites because floor-divide caps each tile at tile**ndim cells). One law, two storage shapes. + +FIRST MIGRATION (proven byte-identical): splat_bundle_tiled / recall_region_tiled now DELEGATE their tiling +to TiledStore + _tile_bucket. The splat module owns only its encode (role-bound occupancy) and decode; the +floor-divide routing and bounded grouping live once, in the shared store. A PARITY TEST recomputes every +tile bundle with the OLD inline (gy//tile, gx//tile) logic and asserts np.array_equal -- the delegation +changed NOTHING, bit-for-bit. Build-time and recall-time tiling now provably route identically (same +_tile_bucket), closing a latent class of "the two floor-divides drifted" bug. + +THE TWO RULES STILL HOLD (and are now enforced in ONE place instead of rediscovered per caller): KEY ON THE +ITEMS THEMSELVES (a tree only routes when query ~= key; a weakly-correlated summary mis-routes -- the +~0.27-cosine measurement), and NEVER STORE THE INDEX AS A BUNDLE (decode-via-cleanup caps with set size). +TiledStore is the sanctioned exception to the second: it bundles, but only within a bounded-load tile, which +is exactly why it is a separate object with its own docstring saying so. + +STILL TO MIGRATE (the remaining clients, each its own backward-compatible + parity-tested increment, in risk +order): RouteIndex -> keying='sequential' (the two-level chunk-summary, its own docstring already admits it +is "this index at its small-n operating point"); the direct HoloForest-wrapping sites (ablate, creature, +denoise/NLM, mind, unified, uri) -> structured_index(keying='projection') (byte-identical, removes the near- +copies); FacetStore -> structured-address keying + recursion (its bi-level "prefix outside, forest inside" IS +index-of-indexes). And the optional axes the RAM thread named but this increment did not need yet: raid=True +(shards backed by HoloArray -- parity + grow), and halo= for the coupling operations (the convolution-as-bind +tiling, where a feature near a tile edge spreads into the neighbour -- overlap-add, which bind's bilinearity +makes clean: bind(f, g) = sum over tiles of bind(f_tile, g)). + +SEATS: Pharr (the BVH / acceleration-structure framing of the index), Duda (the addressing / page-table +information view), Stoudenmire (the low-rank shard view) -- and the RAM/addressing thread's own systems- +engineering lesson: fifty years of storage and interconnect design converges on "balanced tree of fixed- +budget nodes, routed by a pivot or a structured address, never summarising content upward." The engine +keeps re-deriving it. + +Tests: +10 (1194 -> 1204). test_holographic_tree.py (+7): hash keying is zero-comparison exact (~1 +comparison at N=2000) and returns None for absent keys; the hash route is deterministic across processes +(blake2b, not salted hash); hash carries payloads; spatial routes by floor-divide and is exact; locate_exact +agrees with the routed locate for the computed keyings; locate_k refuses non-projection keyings (k-NN is a +content query); TiledStore routes + groups with bounded per-tile load. test_holographic_splat.py (+1): the +byte-identical migration parity test (new tiles == old inline tiling, np.array_equal). test_integration.py +(+2): the three keying regimes reachable through the mind faculty, and the splat tiler + spatial index +provably sharing ONE route. Files: holographic_tree.py, holographic_splat.py, holographic_unified.py, +test_holographic_tree.py, test_holographic_splat.py, test_integration.py, tour.py, README, NOTES_concepts.md. + + +-------------------------------------------------------------------------------- +CONSOLIDATION, increment 2 -- RouteIndex -> the shared 'sequential' keying. The route chunker's two-level +random-access index (nearest chunk SUMMARY, then nearest tile within the chunk) was the next member of the +chunking family to fold onto the one fabric. It is now a keying on StructuredIndex, and RouteIndex delegates. + +WHAT SHIPPED (additive, backward-compatible): + * StructuredIndex gains keying='sequential' (holographic_tree.py): keys are a SEQUENCE of chunks (each a + (chunk_size, dim) array); locate routes two-level by EXACT scan -- argmax over the chunk summaries (a + normalised bundle per chunk), then argmax over the chosen chunk's RAW tiles -- returning the (chunk, + position) coordinate. It reproduces RouteIndex's computation exactly: same normalising bundle for the + summary, query normalised, tiles kept RAW at level 2 (so NO normalisation drift -- the trap that makes the + forest-wrapping sites non-trivial, see below). locate_exact delegates to locate (already exact); locate_k + still refuses (sequential is not nearest-neighbour). + * RouteIndex (holographic_plan.py) now DELEGATES: it keeps its public surface (self.chunks, n_chunks, and a + _summaries property that reads the index's summaries for the determinism audit) and its route-specific + global-step bookkeeping, but the summary computation + two-level routing live in the shared index. A + PARITY TEST recomputes the old inline two-level scan and asserts locate() returns the identical + (chunk, pos, global_step) for every tile on a real route -- byte-identical, the delegation changed nothing. + +WHY THE FOREST-WRAPPING DE-DUPS WERE DEFERRED (the honest call, kept on record): the six direct +HoloForest(...) sites (ablate, creature, denoise, mind, unified, uri) looked like the biggest duplication +win, but they are NOT byte-identical swaps. HoloForest.recall ranks by RAW DOT PRODUCT +(items[cand] @ query); StructuredIndex unit-normalises its keys, so the RP-tree splits differently and the +candidate set can change. And the sites are mostly the wrong shape to migrate blindly: ablate and creature +are benchmark/demo blocks whose ground truth is itself a dot-product argmax; denoise and the unified graph +use recall_k where the normalised tree shifts the neighbours; mind is a performance-critical hot path tied to +ReflexCache; uri belongs to the FacetStore migration. Forcing them would risk behaviour change for little +gain. The clean path for them later is either a normalize=False option on the projection keying, or migrating +only the ones whose vectors are already unit-norm, each with a parity test. Deferred, not forgotten. + +STILL TO MIGRATE (unchanged from increment 1, minus RouteIndex which is now done): FacetStore -> structured- +address keying + recursion (its bi-level "prefix outside, forest inside" IS index-of-indexes -- and its inner +hot-bucket forest is the uri.py site above, so the two land together); the forest-wrapping de-dups (with the +normalisation caveat above); and the optional axes the RAM thread named -- raid=True (HoloArray-backed shards: +parity + grow) and halo= (the convolution-as-bind overlap-add tiling). + +Tests: +2 (1204 -> 1206). test_holographic_plan.py (+1): the RouteIndex byte-identical migration parity test +(locate == old inline two-level scan for every tile; summaries bit-identical). test_integration.py (+1): +RouteIndex's routing IS StructuredIndex(keying='sequential'), and an independent sequential index over the +same chunks routes a tile to the same (chunk, position). Files: holographic_tree.py, holographic_plan.py, +test_holographic_plan.py, test_integration.py, tour.py, README, NOTES_concepts.md. + + +-------------------------------------------------------------------------------- +CONSOLIDATION, increment 3 -- the content store delegates, and the forest-de-dup unlock ships. The deferred +forest-wrapping sites were blocked by a real mismatch: HoloForest.recall ranks by RAW DOT PRODUCT, while +StructuredIndex unit-normalised its keys (so the RP-tree split differently). This adds the one parameter that +removes the block and migrates the most on-theme site -- the content store, which StructuredIndex's own +docstring already CLAIMED was "this index at its at-scale operating point" but which was still wrapping a raw +forest itself. + +WHAT SHIPPED (additive, backward-compatible): + * StructuredIndex projection keying gains normalize=True (default = unchanged). normalize=False keeps keys + RAW, so the tree splits on raw vectors and locate ranks by raw dot product -- making the index + BYTE-IDENTICAL to a bare HoloForest over the same items. MEASURED: 0/300 query mismatches vs a bare forest + on deliberately non-unit-norm items. This is the unlock the increment-2 note flagged: a site that already + wraps a raw forest can now delegate with zero behaviour change. + * FacetStore (holographic_uri.py) now DELEGATES its hot-bucket content search to StructuredIndex + (keying='projection', normalize=False), filing each record under its own content vector and carrying the + record as the payload. build_indexes builds the shared index; nearest() calls locate (returning the record + + the cost directly). A PARITY TEST proves nearest() returns the SAME record a bare HoloForest would, for + every query -- so the docstring's claim is now literally true, not aspirational. The prefix-LISTING layer + (put / list / common_prefixes / tree) is untouched: it is a distinct *listable keyspace* capability, a + sibling to the lookup index, not a keying of it (the honest scoping from increment 2's findings). + +WHAT'S DELIBERATELY NOT DONE (and why -- keep the negatives loud): the other five forest-wrapping sites are +now UNBLOCKED (normalize=False makes them byte-identical too, including the recall_k sites since recall_k +already ranks by cosine over whatever tree was built), but they are NOT worth migrating right now: ablate and +creature are benchmark/demo blocks (calling the forest directly is not a "duplicate implementation", it is +just USING the primitive); mind is a performance-critical hot path tied to ReflexCache where the payload +indirection buys nothing; denoise/unified-graph are recall_k uses that would gain only indirection. Migrating +them would add wrapper overhead for no functional gain. The de-dup that MATTERED was the content store (a +genuine "individual solution" in the chunking/store family, and the one the docstring named); that is done. + +WHERE THE CONSOLIDATION STANDS: the routing fabric (StructuredIndex: projection / hash=RAM / spatial / +sequential keying, + normalize toggle, + TiledStore for the decode-a-bundle storage axis) is built, and the +three genuine "individual solutions" in the chunking/tiling/store family now delegate to it -- splat tiler +(spatial), RouteIndex (sequential), FacetStore hot bucket (projection). The capacity-cliff cure lives ONCE. +Remaining are EXTENSIONS, not consolidations: the optional axes the RAM thread named -- raid=True (HoloArray- +backed shards: parity + grow, for the at-scale degradation-tolerant case) and halo= (the convolution-as-bind +overlap-add tiling, where bind's bilinearity makes the tiling clean: bind(f,g) = sum_tiles bind(f_tile, g)). +Those are net-new capability and can be picked up from the backlog rather than as cleanup. + +Tests: +3 (1206 -> 1209). test_holographic_tree.py (+1): normalize=False is byte-identical to a bare +HoloForest (0/300 mismatches on non-unit-norm items). test_holographic_uri.py (+1): FacetStore.nearest() +returns the same record as the bare forest it replaced, every query. test_integration.py (+1): a hot bucket's +content search IS a StructuredIndex (keying='projection'), the de-siloing made real. Files: holographic_tree.py, +holographic_uri.py, holographic_unified.py, test_holographic_tree.py, test_holographic_uri.py, +test_integration.py, README, NOTES_concepts.md. + + +-------------------------------------------------------------------------------- +FWD-7 -- the explicit mesh can finally be EDITED, not just described. The kernel (FWD-1) shipped the half-edge +substrate and the Euler invariants but was effectively read-only. This adds the LOCAL EULER OPERATORS -- the +bounded connectivity rewrites known since Baumgart/Mantyla that every higher modeling op (subdivide, bevel, +decimate, remesh) decomposes into. + +SCOPE (honest): this ships the PRIMITIVE Euler-operator layer (flip / split / collapse / split_face) -- the +substrate FWD-7's user-facing modeler VERBS (extrude / bevel / inset / loop-cut / bridge / dissolve) decompose +into (extrude = a loop of MEV+MEF, loop-cut = a ring of split_edge, dissolve = KEV/KEF). Those verbs are the +REMAINING FWD-7 work. The primitives already satisfy FWD-7's stated bar -- manifold-preserving on valid input, +deterministic per the ISA contract, and undoable via the make/kill round-trips -- so this is the foundation +the verbs stand on, not FWD-7 complete. NOTE ON ORDERING: the forward backlog puts Tier 1 (FWD-3/4/5/6, the +ADAPT-SHIPPED items that wire chart/graphsignal/steering onto meshes) ABOVE FWD-7 (Tier 2) by leverage; these +primitives were built first as the natural continuation of the kernel, but Tier 1 is the higher-leverage path +and is where the work resumes next. The panel converged here over the bigger forward items: the [Stam] seat +(subdivision surfaces ARE sequences of Euler operators; his exact Catmull-Clark evaluation is the rigor +reference) called it the floor everything else stands on; the [Pharr] seat set the bar (the result must stay +a renderer-valid manifold, not merely "run"); the [Cranmer] seat supplied the measurement (make/kill inverse +pairs give an exact do-then-undo round-trip -- the cleanest correctness witness, not an in-sample fit). FWD-11 +(the mesh<->SDF<->splat VSA bridge, the [Plate]/[Quilez]/[Drettakis] seats) is the higher-value follow-on but +bigger and benefits from having real edit operators first. + +WHAT SHIPPED (holographic_eulerops.py; additive, four standalone operators + four UnifiedMind faculties): + * flip_edge(mesh, a, b) -- rotate the shared edge of two triangles. V/E/F (hence chi) unchanged: the + purest rewrite. The Delaunay-remeshing primitive. Its own inverse (flipping the new edge c-d restores the + old). PRECONDITION kept loud: refuses if c-d already exists (would be shared by 3 faces -> non-manifold). + * split_edge(mesh, a, b) -- insert a midpoint vertex, splitting the incident triangle(s). V+1, chi + unchanged. Returns (new_mesh, m). The refinement primitive. + * collapse_edge(mesh, keep, remove) -- the INVERSE of split_edge: merge an edge's endpoints. V-1, chi + unchanged. The decimation/LOD primitive. GUARDED by the LINK CONDITION: keep and remove may share + neighbours only at the apexes of their shared faces; otherwise the contraction would weld the surface + onto itself, so it returns None. Not every edge is collapsible -- a true property of meshes, made + operational (the caller must handle the refusal), not a code shortcoming. + * split_face(mesh, f, i, j) -- cut a polygon with a diagonal between two corners (MEF). E+1, F+1, chi + unchanged. The one operator that works on n-gons, not just triangles. + +DESIGN CHOICE (readability over pointer surgery): each operator uses the half-edge adjacency to FIND the local +patch (which faces share the edge, the opposite apexes) then REWRITES THE FACE LIST and lets the new Mesh +rebuild its own half-edge table -- so the combinatorics stay legible and there is no cache to invalidate or +twin pointer to fix by hand. Cost: a rebuild per edit, consistent with the kernel's recorded "NumPy is the +wrong tool for per-element mesh loops" negative (which this module inherits and re-flags). + +DETERMINISM (ISA EXACT class): new vertices are APPENDED (index = old count); faces rewritten in face order; +vertex removal reindexes by one fixed rule (drop the index, decrement higher ones). Pure function of +(mesh, selection) -> byte-identical out (asserted). No float comparison ever chooses connectivity. + +KEPT NEGATIVES: (1) collapse_edge is not always legal (link condition) -- refuses rather than break the mesh; +(2) flip into an existing edge is illegal -- refuses; (3) flip/split/collapse require triangle faces on the +touched faces and raise otherwise (split_face is the n-gon operator); (4) the per-element Python-loop bound +remains -- fine for interactive single edits, a compiled core is still the eventual need for heavy remeshing. + +Tests: +12 (1209 -> 1221). test_holographic_eulerops.py (+11): flip chi/V/E/F-invariance and flip-back +round-trip; the flip duplicate-edge refusal; split_edge vertex-add + chi preservation; the split->collapse +exact make/kill round-trip; the collapse link-condition refusal (bipyramid equator) and a legal collapse +(bipyramid -> tetrahedron); split_edge's non-triangle rejection; split_face n-gon chi preservation and its +adjacent-corner rejection; operator determinism. test_integration.py (+1): the operators as UnifiedMind +faculties preserving the invariants end-to-end (split->collapse restores; flip stays a closed manifold). +Files: holographic_eulerops.py (new), test_holographic_eulerops.py (new), holographic_unified.py (4 faculties), +test_integration.py, README, NOTES_concepts.md, tour.py. + + +-------------------------------------------------------------------------------- +FWD-4 -- the first TIER 1 ADAPT-SHIPPED item: mesh smoothing is the shipped Taubin filter, wired onto a mesh. +The forward backlog's real insight is that the matured intrinsic-geometry toolkit turns the "conventional" DCC +items into adaptations of shipped faculties, and the panel converged here (over the out-of-order FWD-7 +primitives) because Tier 1 is the highest-leverage path AND valuable under either fork of the native-vs-mesh +strategic decision. FWD-4 is the cleanest of the four: `graphsignal.taubin_filter(vectors, nbr_idx, nbr_w)` +already exists and is tested, so mesh smoothing is THREE substitutions and nothing else -- vertex positions as +the signal, the mesh 1-ring as `nbr_idx`, cotangent weights as `nbr_w`. This is a WIRE, not a re-implementation. + +WHAT SHIPPED (holographic_meshsmooth.py; additive; one UnifiedMind faculty `mesh_smooth`): + * cotangent_adjacency(mesh) / uniform_adjacency(mesh) -> (nbr_idx, nbr_w) in the (V, k_max) padded, + row-normalised format the shipped filter consumes. COTANGENT = the discrete Laplace-Beltrami weight + w_ij = (cot a + cot b)/2 over the two adjacent triangles' opposite angles -- geometry-aware (accounts for + triangle shape), so it approximates true surface diffusion, not mesh-connectivity diffusion. Triangulates + internally (cotangents are triangle angles); clamps negative (obtuse) weights to >= 0. + * taubin_smooth(mesh, lam, mu, iters, weights) -> a new Mesh, smoothed positions, FACES UNTOUCHED (so all + connectivity and Euler invariants preserved -- only vertices move). Delegates the filtering to the shipped + graphsignal.taubin_filter. laplacian_smooth ships as the shrinking baseline. + +WHY TAUBIN not naive Laplacian: lambda-only Laplacian smoothing SHRINKS the surface toward its centroid; +Taubin alternates a shrink (lambda>0) and a larger un-shrink (mu<0, |mu|>lambda) step, preserving low-frequency +extent while removing high-frequency noise. + +MEASURED (the [Milanfar] denoiser-as-manifold-map bar, the [Cranmer] no-shrink test): on a noisy unit sphere +(subdiv-3 icosphere, sigma=0.05), Taubin cut radial error 0.0400 -> 0.0177 (a 56% denoise) and KEPT the mean +radius at 1.009 (no shrink), while the naive Laplacian collapsed the mean radius to 0.894. Connectivity and chi +unchanged. Deterministic (byte-identical positions run-to-run). + +KEPT NEGATIVES (loud): + * Fixed strength over-smooths an already-clean mesh (it is a low-pass) -- proper use needs a noise estimate; + the faculty exposes lam/mu/iters and does NOT auto-tune (the sigma-estimate discipline, deferred). + * COTANGENT IS NOT UNIFORMLY BETTER. On THIS regular sphere with isotropic noise, UNIFORM weights denoise a + touch better (0.0146 vs 0.0177) -- a near-regular mesh with directionless noise has no triangle-shape + variation for cotangent to exploit. Cotangent's real edge is IRREGULAR meshes / feature preservation; it is + the default for that reason, but we keep the honest finding rather than assert a superiority that isn't + there in this case. + * Cotangent weights can go negative on obtuse triangles -> clamped to >= 0 (the standard intrinsic/clamped + cotangent mitigation; exact on well-shaped meshes, a documented approximation on very obtuse ones). + +ORDERING NOTE: this resumes the backlog at its real highest-leverage point. Remaining Tier 1: FWD-6 (curvature +/ feature detection via steering + Laplacian -- which FWD-4's crease-aware mode wants), then FWD-3 (UV via +manifold_chart on mesh edges) and FWD-5 (geodesics via chart.geodesic_distances), both of which need the +seam/atlas machinery (ARCH-4) because they replace manifold_chart/geodesic_distances' k-NN graph with explicit +mesh edges. The FWD-7 modeler verbs (extrude/bevel/inset/loop-cut/bridge/dissolve) stand on the already-shipped +primitive Euler operators and are Tier 2. + +Tests: +10 (1221 -> 1231). test_holographic_meshsmooth.py (+9): Taubin denoises; no-shrink; the Laplacian +baseline shrinks; connectivity + chi preserved (vertices-only); both weightings denoise (no false cotangent +superiority); adjacency is row-normalised + rectangular; cotangent weights non-negative; quad topology kept; +determinism. test_integration.py (+1): mesh_smooth as a UnifiedMind faculty denoising without shrink end-to-end. +Files: holographic_meshsmooth.py (new), test_holographic_meshsmooth.py (new), holographic_unified.py (faculty), +test_integration.py, README, NOTES_concepts.md, tour.py. + + +-------------------------------------------------------------------------------- +FWD-6 -- Tier 1, item four: mesh curvature & feature detection, the item with the most RIGOROUS reference of +the forward set because discrete differential geometry hands us exact identities to check against. Three +measurements, each reusing shipped machinery or grounded in a hard invariant: + +WHAT SHIPPED (holographic_meshcurvature.py; additive; three UnifiedMind faculties + a confidence faculty): + * mean_curvature(mesh) -> |H| via the discrete mean-curvature-normal operator K(x_i)=(1/A_i) sum_j w_ij + (x_i-x_j) = 2 H_i n_i, with w_ij the cotangent edge weights REUSED from FWD-4 (curvature and smoothing are + the same operator -- one applied, one measured) and A_i the barycentric vertex area. On a unit sphere + H=1/R=1. + * gaussian_curvature(mesh) -> angle defect / area (K=1/R^2=1 on the unit sphere), with the strongest check in + the module: gauss_bonnet_defect(mesh) = (total angle defect) - 2*pi*chi, which is ~0 to FLOATING POINT for + a closed mesh by discrete Gauss-Bonnet -- the curvature estimate validated against the Euler characteristic + FWD-1 computes. + * dihedral_angles(mesh) / detect_creases(mesh, threshold_deg) -> sharp-edge detection via the angle between + adjacent face normals. A cube's 12 edges are 90-degree creases; a smooth sphere has none. + * curvature_confidence(mesh) -> per-vertex [0,1] reliability from 1-ring regularity (the noise negative made + actionable). + +THE [MILANFAR] STRUCTURE-TENSOR / STEERING CONNECTION: curvature is the surface's local shape -- the directions +and rates it bends -- which is exactly the anisotropic local metric a steering kernel encodes (structure tensor +on geometry, not image gradients). The curvature field + crease set are what an adaptive operator STEERS by +(subdivide where |K| is high, smooth along creases not across them, split shading normals at sharp edges). This +ships the scalar curvatures + crease set (the measurable core); the anisotropic steering of downstream +operators is the consumer. + +MEASURED (the exact references): on a subdiv-3 unit icosphere -- Gauss-Bonnet total defect = 2*pi*chi to 6e-14 +(machine precision); mean Gaussian K=1.022, mean |H|=1.010 (both ~1); per-vertex coefficient-of-variation 0.07 +(the noise negative). On a cube -- exactly 12 creases, each a 90-degree dihedral; the 6 flat triangulation +diagonals correctly excluded; a smooth sphere yields 0 creases. Deterministic. + +KEPT NEGATIVES (loud): + * Per-vertex curvature is NOISY on coarse/irregular meshes -- the MEAN over a closed surface is accurate and + the Gauss-Bonnet TOTAL is exact, but individual vertex values vary (CoV 0.07 even on a regular sphere). The + estimate needs a reasonably regular 1-ring; curvature_confidence scores per-vertex reliability so a caller + can down-weight rather than trust blindly. + * The exact Gauss-Bonnet check is for CLOSED meshes; an open mesh carries a boundary (geodesic-curvature) + term, so its total defect is not 2*pi*chi and the check is skipped there. + * Angle defect + the cotangent operator assume TRIANGLE faces; n-gons are triangulated for the computation + (face normals / dihedral use Newell so they work on n-gons directly). + +REFACTOR (additive, backward-compatible): exposed `cotangent_edge_weights(mesh)` from holographic_meshsmooth +(the raw, un-clamped Laplace-Beltrami edge weights), used internally by FWD-4's cotangent_adjacency and reused +by FWD-6's mean_curvature -- so the cotangent computation lives in one place. FWD-4's selftest + 9 tests +re-verified green after the refactor. + +Tests: +13 (1231 -> 1244). test_holographic_meshcurvature.py (+12): Gauss-Bonnet exact on a closed mesh (and on +a cube); unit-sphere mean/Gaussian curvature ~1; barycentric vertex areas sum to surface area; cube = 12 +creases at 90deg; triangulated cube still 12 (flat diagonals excluded); smooth sphere = 0 creases; per-vertex +noise negative; confidence in range; determinism. test_integration.py (+1): curvature + creases as UnifiedMind +faculties end-to-end with the exact Gauss-Bonnet validation. Files: holographic_meshcurvature.py (new), +test_holographic_meshcurvature.py (new), holographic_meshsmooth.py (additive cotangent_edge_weights), +holographic_unified.py (3 faculties), test_integration.py, README, NOTES_concepts.md, tour.py. + + +-------------------------------------------------------------------------------- +FWD-5 -- Tier 1, the geodesic item: distance ALONG the surface, and the foundation FWD-3 (UV) stands on. The +shipped chart.geodesic_distances computes geodesics as shortest paths on a graph (Floyd over a k-NN graph), and +chart.classical_mds embeds any distance matrix. The honest ADAPT-SHIPPED move: run the same shortest-path idea +on the EXPLICIT MESH EDGE graph (true surface connectivity, real Euclidean edge lengths) instead of a k-NN +approximation. FWD-3 will feed the resulting geodesic matrix to the shipped classical_mds for the UV chart. + +WHAT SHIPPED (holographic_meshgeodesic.py; additive; two UnifiedMind faculties): + * geodesic_distances(mesh, source) -- single-source Dijkstra along mesh edges (Euclidean weights) -> distance + to every vertex. Along the surface, not the straight line through the void. + * geodesic_matrix(mesh) -- all-pairs (repeated Dijkstra); the distance matrix FWD-3's classical-MDS UV chart + consumes. + * geodesic_soft_selection(mesh, source, radius, falloff) -- a [0,1] falloff by geodesic distance that does NOT + bleed to vertices near in 3-D space but far across the surface (the geodesic-vs-Euclidean win). + +MEASURED (vs the analytic great circle on a unit sphere): geodesic from the pole correlates with arccos(z) at +corr=0.994; antipode (north->south) geodesic = 3.136 ~ pi, GREATER than the Euclidean diameter (2.0). The +geodesic-vs-Euclidean contrast made concrete: a soft selection at radius 2.5 EXCLUDES the antipode (geodesic +~pi > 2.5) that a Euclidean ball of the same radius would INCLUDE (straight-line 2.0 < 2.5) -- the "even on the +surface, no bleed" property. Deterministic (Dijkstra ties break on integer vertex index). + +KEPT NEGATIVES (loud): + * The edge-graph geodesic is APPROXIMATE. It overestimates the polyhedron's own face-crossing geodesic (edge + restriction), and vs a SMOOTH surface sits a few percent high overall (+7.7% net on the sphere) with a tiny + chord-effect undercut possible near the source (edges are chords, slightly shorter than arcs; <1%). The + earlier draft asserted a clean ">= true geodesic" bound -- that is WRONG against a smooth reference (61/258 + sphere vertices slightly undercut), so the test now measures the net overestimate AND the bounded undercut + rather than claim a one-sided bound that does not hold. Accept where the mesh is fine and curvature mild. + * All-pairs is O(V * E log V) -- fine here, not for very large meshes; a heat-method solve is the next step. + +Tests: +10 (1244 -> 1254). test_holographic_meshgeodesic.py (+9): great-circle correlation; net overestimate +with bounded undercut; antipode farthest + exceeds Euclidean; soft-selection excludes the antipode a Euclidean +ball includes (no bleed); soft-selection in range; matrix symmetric + zero diagonal; reachability on a closed +mesh; flat-grid along-grid >= straight-line sanity; determinism. test_integration.py (+1): geodesic + soft +selection as UnifiedMind faculties with the geodesic-vs-Euclidean contrast end-to-end. Files: +holographic_meshgeodesic.py (new), test_holographic_meshgeodesic.py (new), holographic_unified.py (2 faculties), +test_integration.py, README, NOTES_concepts.md, tour.py. NEXT: FWD-3 (UV unwrapping) feeds geodesic_matrix to +the shipped classical_mds -- the last Tier 1 item, needing seam handling for closed surfaces (ties to ARCH-4). + + +-------------------------------------------------------------------------------- +FWD-3 -- Tier 1 CLOSED: UV unwrapping, the payoff of FWD-5 and the backlog's sharpest irony. UV, the +"least-holostuff" item on the original DCC list, is a near-direct reuse of shipped, tested faculties. The shipped +chart.classical_mds embeds any distance matrix; chart.manifold_chart's Isomap is exactly "classical MDS of the +GEODESIC matrix". So UV unwrapping = feed the mesh's OWN geodesic distances (FWD-5's geodesic_matrix, on explicit +edges) to the shipped classical_mds; the 2-D embedding IS the UV chart. Machinery shipped, substitution is "mesh +geodesics in place of k-NN geodesics". + +WHAT SHIPPED (holographic_meshuv.py; additive; two UnifiedMind faculties): + * uv_unwrap(mesh, method) -- (V,2) UV packed to ~[0,1]^2. 'isomap' (geodesic-preserving, wins on curved), + 'planar' (linear PCA, exact on developable), 'spectral' (Laplacian eigenmaps). + * uv_distortion(mesh, uv) -- per-edge STRETCH spread (log-ratio std): 0 = isometric, grows with curvature. + * flat_grid_mesh / hemisphere_cap / puncture -- developable reference, curved test surface, closed->disk seam. + +MEASURED: flat isotropic patch unwraps near-isometric (stretch spread 0.049); curved hemisphere cap 0.233 +(Gauss -- unavoidable); punctured sphere 0.507 (closed needs a real seam). The bar's "charts don't overlap after +packing" met EXACTLY: both flat and cap unwraps are 100% orientation-consistent (zero flipped triangles -> +locally injective). The bar's "beats a baseline": on the CURVED cap, Isomap (0.233) beats a naive linear PCA +projection (0.456) -- geodesic preservation wins where the surface bends. + +KEPT NEGATIVES (loud): + * Disk-topology required. A CLOSED surface has no boundary and cannot flatten to a disk without a CUT; direct + unwrapping distorts badly. puncture() opens it crudely (one vertex), and the test MEASURES the punctured + sphere distorting far more than a cap -- the seam-need made concrete. A good seam (a cut path placed by the + topology/genus faculty) is ARCH-4, deferred. + * On a DEVELOPABLE (flat) surface the linear 'planar' projection is EXACTLY isometric (0.000) and BEATS Isomap + (0.049) -- Isomap carries the edge-graph geodesic's small approximation error. Isomap is NOT universally + better; it wins on curved surfaces (its purpose) and slightly loses to linear on flat ones. This mirrors + chart.py's own philosophy ("linear SVD remains the right choice when the manifold is flat"). Pick by curvature. + * The unwrap is sensitive to triangulation ANISOTROPY: a FAN triangulation (all diagonals one way) biases the + edge-graph geodesic and distortion GROWS with resolution (measured 0.14 -> 0.17, 5x5 -> 15x15); an ISOTROPIC + (alternating-diagonal) mesh behaves correctly, distortion SHRINKS toward isometric as it refines + (0.063 -> 0.044). flat_grid_mesh uses alternating diagonals for this reason -- the bias is documented, not + hidden by picking a passing triangulation. (This diagnosis corrected a first-draft threshold of <0.06 on a + fan grid that fails at 0.14; the honest finding is the anisotropy, kept on record.) + +Tests: +10 (1254 -> 1264). test_holographic_meshuv.py (+9): flat near-isometric; non-degenerate UV; no flipped +triangles (no overlap, both surfaces); curved cap distorts more than flat; Isomap beats planar on curved; planar +beats Isomap on flat (the honest reverse, planar exact); punctured sphere is a disk and distorts most; unit-square +packing; determinism. test_integration.py (+1): UV unwrap through the mind, flip-free, Isomap-beats-planar on the +cap end-to-end. Files: holographic_meshuv.py (new), test_holographic_meshuv.py (new), holographic_unified.py +(2 faculties), test_integration.py, README, NOTES_concepts.md, tour.py. + +TIER 1 COMPLETE: FWD-4 (smoothing), FWD-5 (geodesics), FWD-6 (curvature/creases), FWD-3 (UV) all shipped -- the +cheap, high-leverage wires that turn the matured intrinsic-geometry toolkit (chart, graphsignal, steering, +spectral-iteration) onto explicit meshes. NEXT: Tier 2 -- FWD-7 modeler VERBS (extrude/bevel/inset/loop-cut/ +bridge/dissolve, decomposing into the shipped primitive Euler operators), FWD-8 subdivision (reuses +spectral-iteration), FWD-9 rig/skinning (reuses moe), FWD-10 IK (reuses iterate-a-projection); then Tier 3 +FWD-11 (mesh<->SDF<->splat bridge). ARCH items interleave (ARCH-4 atlas/seams would give FWD-3 a real seam). + + +-------------------------------------------------------------------------------- +FWD-7 (core) -- Tier 2 LEAD: the modeller VERBS, built on the explicit mesh kernel. The shipped primitive Euler +operators (holographic_eulerops: flip/split_edge/collapse/split_face) are the atomic invariant-preserving moves; +these are the human-facing operations on top. Backlog thesis: the verbs DECOMPOSE into Euler operators -- the +honest frame, with one explicit caveat (extrude needs MEV, which the four shipped primitives don't include, so it +is a direct face-list construction in the primitives' style, not a literal call sequence we can't make). + +WHAT SHIPPED (holographic_meshverbs.py; additive; three UnifiedMind faculties): + * extrude_face(mesh, face, distance) -- lift a face along its normal + side walls. The iconic verb. + * inset_face(mesh, face, ratio) -- shrink a face toward its centroid + surrounding ring (in-plane). + * dissolve_vertex(mesh, vertex) -- remove a vertex + its umbrella, fan-triangulate the hole (Euler KEV). The + decimation cousin collapse_edge (shipped) instead merges the vertex onto a neighbour. + Shared helper _ring_walls wires the side/ring walls with windings that SUPPLY the freed directed edges (the + manifold-balance condition). Walls triangulated -> output stays pure-triangle (safe for cotangent/curvature). + +MEASURED -- each verb produces a VALID mesh (the bar) with an EXACT geometric signature: + * All three PRESERVE chi (=2) and keep a closed mesh CLOSED + MANIFOLD, on both a triangle mesh (icosphere) and + a QUAD mesh (box, degree-3 vertices) -- robust across mesh types. + * extrude: cap moves EXACTLY `distance` (0.300) along the face normal and ONLY along it; outward extrude + increases signed volume (box 1.0 -> 1.5). + * inset: central-face area EXACTLY (1-ratio)^2 of the original; central face stays coplanar (normal preserved). + * dissolve: removes EXACTLY one vertex (icosphere 66 -> 65). + Deterministic (new vertices are pure functions of input positions; faces appended in fixed order). + +KEPT NEGATIVES / SCOPE (loud): + * CORE three only. bevel, bridge, loop-cut are the FWD-7 remainder, deferred: bevel/bridge need vertex + DUPLICATION with an offset/correspondence (fiddly, easy to get subtly wrong); a general loop-cut needs robust + loop tracing on an arbitrary triangle mesh. Three correct measured verbs > six shaky ones. + * extrude is NOT a literal composition of the four shipped primitives (it needs MEV, not in the set) -- the + decomposition is the conceptual model, the direct construction is the honest implementation. Said plainly in + the module docstring rather than overclaimed. + * dissolve_vertex fan-triangulates the hole from one ring vertex -- valid TOPOLOGICALLY (covers the polygon, + stays manifold) but not a quality remesh for a wildly non-convex link; a curvature-aware fill is out of scope. + +Tests: +11 (1264 -> 1275). test_holographic_meshverbs.py (+10): extrude/inset/dissolve each preserve +chi+closed+manifold; extrude cap moves exactly distance along normal; extrude increases volume; inset area = +(1-ratio)^2; inset coplanar; dissolve removes one vertex; all three on a quad box; determinism. +test_integration.py (+1): the three verbs through the mind with the exact extrude signature end-to-end. Files: +holographic_meshverbs.py (new), test_holographic_meshverbs.py (new), holographic_unified.py (3 faculties), +test_integration.py, README, NOTES_concepts.md, tour.py. NEXT: FWD-7 remainder (bevel/bridge/loop-cut) OR +FWD-8 subdivision (reuses spectral-iteration), FWD-9 rig/skinning (reuses moe), FWD-10 IK (iterate-a-projection). + + +-------------------------------------------------------------------------------- +FWD-8 -- Tier 2: mesh subdivision (Loop, for triangle meshes). Subdivision is two operations braided, and naming +them honestly is the point: (1) REFINE -- split each triangle into 4 (a topological op; an Euler-operator +sequence per the Stam seat; the genuinely NEW part), and (2) SMOOTH -- move every vertex to a Loop-weighted +neighbour average (a graph-signal LOW-PASS, the SAME family as FWD-4's Taubin on the shipped graphsignal, whose +smooth limit lives in the low-frequency eigenspace holographic_spectral computes -- the "reuses spectral-iteration" +half). So: refinement new, smoothing is the spectral low-pass the engine already owns. + +WHAT SHIPPED (holographic_meshsubdiv.py; additive; one UnifiedMind faculty): + * loop_subdivide(mesh, levels=1) -- Loop subdivision with the proper masks: interior edge vertex 3/8(a+b)+ + 1/8(c+d), boundary edge midpoint; interior vertex reposition (1-n*beta)v + beta*sum(nbrs) with Warren's + beta=(1/n)(5/8-(3/8+1/4 cos 2pi/n)^2), boundary 3/4 v + 1/8(prev+next); retriangulate 1->4. Non-triangle + input is triangulated first (Loop is a triangle scheme). Returns a new triangle Mesh. + +MEASURED (the bar -- a valid mesh with the exact subdivision properties): + * Each level multiplies faces by EXACTLY 4 (icosphere(1) 32 -> 128 -> 512) and gives V'=V+E (one new vertex + per edge; 18+48=66). chi preserved, closed mesh stays a closed manifold. + * AFFINE REPRODUCTION (the exact rigor reference, the Stam seat's ask): a FLAT mesh stays flat to machine + precision (<1e-12 in z) because the Loop masks are barycentric -- the discrete analogue of Catmull-Clark + reproducing a plane. + * SMOOTHING (low-pass signature, made geometric): dihedral-angle spread on a cube drops 0.740 -> 0.102 over + two levels -- the low-pass character of the smoothing step, dramatic and clear. + Deterministic (weighted averages; edges visited in sorted order -> fixed new-vertex indices). + +KEPT NEGATIVES (loud): + * Loop is a TRIANGLE scheme. A quad mesh (box) is triangulated first, so the result reflects that + triangulation, not a Catmull-Clark quad refinement. Catmull-Clark (the quad scheme) is a separate operator, + not shipped. + * The limit surface is NOT the input's circumscribed smooth shape -- subdividing an inscribed icosphere does + not reproduce the exact sphere (subdivision surfaces have their own limit). The exact-reproduction guarantee + is for AFFINE/planar input only; for curved input the scheme smooths toward its own limit (the honest claim). + +Tests: +9 (1275 -> 1284). test_holographic_meshsubdiv.py (+8): faces x4; V'=V+E; chi + closed manifold; flat +stays flat (affine reproduction, exact); smooths an angular cube (spread roughly halved or more); two levels x16; +all-triangle output from a quad input; determinism. test_integration.py (+1): subdivision through the mind -- +quadruple, chi/manifold preserved, flat-stays-flat, cube-smoothed end-to-end. Files: holographic_meshsubdiv.py +(new), test_holographic_meshsubdiv.py (new), holographic_unified.py (1 faculty), test_integration.py, README, +NOTES_concepts.md, tour.py. NEXT: FWD-9 rig/skinning (reuses moe), FWD-10 IK (iterate-a-projection); or the +FWD-7 remainder (bevel/bridge/loop-cut); then Tier 3 FWD-11 (mesh<->SDF<->splat bridge). + + +-------------------------------------------------------------------------------- +FWD-10 -- Tier 2, the cleanest reuse on the list: inverse kinematics (FABRIK) expressed LITERALLY through the +shipped iterate-a-projection engine. IK asks: given a chain of fixed-length bones and a TARGET for the tip, where +must the joints go so the tip reaches it while every bone keeps its length? FABRIK (Forward And Backward Reaching +IK) is exactly "iterate a projection onto constraints" -- each reaching pass projects each joint onto the sphere +of correct distance from its neighbour, root and target pinned. The engine ALREADY owns that loop: +holographic_denoise.project_onto_constraints (the mind's project_onto_constraints faculty -- Macklin's one object +under the resonator, the PnP denoiser, and PBD) sweeps a list of projection callables in order until they jointly +hold, and that sweep IS FABRIK's forward/backward reaching. So this module does not reimplement the iteration; it +BUILDS the kinematic-chain projections and hands them to the shipped sweeper. Reuse is literal, not a resemblance. + +WHAT SHIPPED (holographic_meshik.py; additive; one UnifiedMind faculty solve_ik): + * solve_ik(joints, target, iters=20, tol=None) -- pose a chain (n+1,3) so the tip reaches target; pure call into + project_onto_constraints over the chain projections. Returns (new_joints, n_sweeps). + * chain(n, length, axis) -- a straight test chain. + Projections: forward reach (pin tip to target, then end->root move inner joint onto radius-L sphere of outer); + backward reach (pin root, then root->end move outer joint onto radius-L sphere of inner). One sweep = one + forward + one backward FABRIK pass. + +MEASURED (the bar): + * REACHABLE target (within total chain length): tip reaches it to <1e-6 in 30 sweeps. + * Every BONE LENGTH preserved to 1e-9 (the hard constraint FABRIK maintains exactly) and ROOT fixed to 1e-12. + * UNREACHABLE target (beyond reach): chain fully EXTENDS -- tip at distance (total length=4.000) from root, + pointing straight at the target (cos > 1-1e-6). The correct degenerate outcome, measured not failed. + * Convergence MONOTONE in sweeps. Works on longer chains (8 bones). Deterministic (pure geometry, no RNG; a + zero-length direction falls back to a fixed axis -- deterministic tie-break, the Macklin bit-exact lesson). + +KEPT NEGATIVES (loud): + * Plain FABRIK has NO joint-angle limits and no obstacle avoidance -- position constraints only. A per-joint + cone projection would slot into the SAME sweep, but is not shipped. + * An UNREACHABLE target cannot be reached by any solver -- the honest outcome is the fully-extended chain. + * FABRIK returns A solution, not THE solution -- a redundant chain has many poses reaching a target; this is + the one the sweep lands on from the given start (deterministic but start-dependent). + +Tests: +9 (1284 -> 1293). test_holographic_meshik.py (+8): reaches reachable target; preserves every bone length; +root fixed; unreachable fully extends; extended chain points at target; convergence monotone in sweeps; longer +chain (8 bones); determinism. test_integration.py (+1): IK through the mind via its own project_onto_constraints, +reachable hit + bones/root preserved + unreachable extended end-to-end. Files: holographic_meshik.py (new), +test_holographic_meshik.py (new), holographic_unified.py (1 faculty solve_ik), test_integration.py, README, +NOTES_concepts.md, tour.py. Tier 2 now: FWD-7 core, FWD-8, FWD-10 shipped. NEXT: FWD-9 rig/skinning (LBS as a +mixture of expert bone-transforms <-> moe); or the FWD-7 remainder (bevel/bridge/loop-cut); then Tier 3 FWD-11. + + +-------------------------------------------------------------------------------- +FWD-9 -- Tier 2, the last core item: skinning/rigging (linear blend skinning) as a SOFT mixture of expert +bone-transforms. Skinning deforms a vertex as a WEIGHTED COMBINATION of what each bone's transform would do to it, +weights summing to one -- structurally a mixture of experts (bones = experts, skin weights = gate). + +THE HONEST REUSE FINDING (reported, not buried -- like FWD-8's spectral nuance): holostuff's mixture of experts +(holographic_moe.GatedMixture) is the HARD, SPARSE, LEARNED kind (top-1 router, gate = the creature brain, only +the chosen expert runs). LBS is the OPPOSITE regime: SOFT, DENSE, FIXED (every bone contributes, painted weights +form a partition of unity, no learning, no winner-take-all). So the moe connection is real but CONCEPTUAL, not a +literal call: skinning is the soft/dense cousin of the engine's hard/sparse GatedMixture. Same experts+gating +skeleton, different gating regime. (Contrast FWD-10, where the iterate-a-projection reuse WAS literal.) + +WHAT SHIPPED (holographic_meshskin.py; additive; one UnifiedMind faculty skin_mesh): + * linear_blend_skin(vertices, transforms, weights) -- v' = sum_b w_b (M_b v); weights row-normalised. (V,3) out. + * skin_mesh(mesh, transforms, weights) -- same, returns a new Mesh (deformed vertices, faces untouched). + * make_transform / rotation(axis,angle) -- build the 4x4 bone transforms (Rodrigues rotation + translation). + +MEASURED (the bar): + * RIGID REPRODUCTION (the partition-of-unity guarantee, LBS's analogue of subdivision's affine reproduction): + if every bone shares one rigid transform M, LBS reproduces M EXACTLY (1e-12) on every vertex for ANY weights. + * Single-bone (weight 1) vertex = exactly that bone's transform; identity transforms leave the mesh fixed; + translation interpolation is the weighted midpoint; skin_mesh leaves faces untouched. Deterministic. + +THE KEPT NEGATIVE, MEASURED TO CLOSED FORM (the point of the module): LBS averages the bone MATRICES, not the +rotations, so a vertex blended 50/50 across a large relative TWIST collapses toward the bone axis (the infamous +"candy-wrapper" artifact). Exact, not vague: a unit ring twisted by theta has blended radius EXACTLY |cos(theta/2)| +-- 0.5 at 120 degrees, 0.000 (full collapse) at 180. The test asserts that closed form at several angles. +Dual-quaternion skinning fixes it by blending rotations properly -- the honest next step, not shipped. + +Tests: +10 (1293 -> 1303). test_holographic_meshskin.py (+9): shared rigid transform reproduced exactly for any +weights; identity fixed; single-bone exact; unnormalized weights treated as partition of unity; translation +interpolation; candy-wrapper = cos(theta/2) at several angles; full collapse at 180; skin_mesh preserves faces; +determinism. test_integration.py (+1): skinning through the mind -- rigid reproduction + faces preserved + the +candy-wrapper closed form end-to-end. Files: holographic_meshskin.py (new), test_holographic_meshskin.py (new), +holographic_unified.py (1 faculty skin_mesh), test_integration.py, README, NOTES_concepts.md, tour.py. + +TIER 2 CORE COMPLETE: FWD-7 core (extrude/inset/dissolve), FWD-8 subdivision, FWD-9 skinning, FWD-10 IK -- the +rig (skeleton+IK) -> skin animation pipeline. The honest reuse ledger across Tier 2: IK's iterate-a-projection was +LITERAL; subdivision's spectral low-pass and skinning's moe-mixture were CONCEPTUAL cousins (named precisely, not +overclaimed). NEXT: FWD-7 remainder (bevel/bridge/loop-cut); Tier 3 FWD-11 (mesh<->SDF<->splat bridge); ARCH items +(ARCH-4 atlas/seams -> a real FWD-3 seam; ARCH-1 StructureRecipe validator+edit-ops mirroring the Euler operators). + + +-------------------------------------------------------------------------------- +FWD-11 -- Tier 3, the highest-value item: the mesh <-> SDF <-> splat bridge. A surface can be carried three ways +-- explicit MESH (verts+faces), implicit SDF (a scalar field, negative inside, zero level-set = the surface), or +SPLAT field (a superposition of Gaussians, holographic_splat). Same geometry, three costumes -- the project's +recurring thesis. This is the bridge that converts between them and measures the round-trip. + +THE GENUINELY NEW PIECE: isosurface extraction (SDF -> mesh). The mesh kernel's own header says "no marching +cubes" -- so extracting a mesh from an implicit field was the one missing direction. Supplied here via MARCHING +TETRAHEDRA (not cubes) on purpose: a tiny unambiguous case set (per tet 0/1/2 triangles by how many of 4 corners +are inside) vs marching cubes' 256 cases + ambiguous faces; and MANIFOLD BY CONSTRUCTION (a crossing lives on a +grid edge shared by every tet touching it, welded by edge identity; the tet's quad split is interior, so adjacent +patches always agree -- no cracks). Cube split into 6 tets sharing a main diagonal (Kuhn decomposition). + +WHAT SHIPPED (holographic_meshbridge.py; additive; two UnifiedMind faculties): + * mesh_from_sdf(sdf, bounds, res, level) [faculty] / marching_tetrahedra(values, axes, level) -- extract the + level-set isosurface of a sampled field as a watertight, OUTWARD-oriented triangle Mesh. The bridge's core. + * mesh_to_sdf(mesh, points) [faculty] -- signed distance from a mesh (vectorised closest-point-on-triangle, + Ericson's region test; sign from the nearest face normal). The reverse direction. + * sample_field / sphere_sdf / metaball_field -- grid sampler, analytic sphere SDF, and the splat-as-implicit + Gaussian sum (a bundle of Gaussians thresholded is an isosurface). + +MEASURED (the bar, against analytic references): + * SDF -> mesh: the analytic unit sphere extracts to a CLOSED MANIFOLD (chi=2), 100% OUTWARD-oriented faces, + vertices on the sphere (mean r=0.999 +/- 0.001). A radius-0.7 sphere -> r=0.699 +/- 0.001. Resolution scales + (res 12/20/28 -> 1536/4392/9216 faces). + * mesh -> SDF: a sphere mesh's signed distance matches analytic |p|-1 at probes (<0.05), correct inside/outside + sign (origin negative, far point positive). + * SPLAT -> mesh: a sum of Gaussian splats (metaball field) iso-extracts to a closed-manifold blob -- the splat + representation entering the mesh world through the SAME extractor. Deterministic. + +KEPT NEGATIVES (loud): + * mesh_to_sdf signs by the NEAREST FACE NORMAL -- exact for convex-ish closed meshes, can mis-sign deep + concavities or thin sheets (generalized winding number is the fix, not shipped). The magnitude is always right. + * Marching-tet resolution is the grid's: sharp features below the cell size are rounded; the round-trip recovers + the SHAPE to grid resolution, not the original connectivity. + * It emits edge-welded triangle soup (no triangle-quality guarantee) -- a downstream Taubin smooth/remesh + (FWD-4) is the cleanup, which is exactly why those faculties exist. + +Tests: +10 (1303 -> 1313). test_holographic_meshbridge.py (+9): SDF->mesh closed manifold sphere; vertices on +sphere; radius scales; 100% outward orientation; resolution scaling; mesh->SDF matches analytic; sign correct; +splat->mesh closed blob; determinism. test_integration.py (+1): the full bridge through the mind (SDF->mesh, +mesh->SDF, splat->mesh) end-to-end. Files: holographic_meshbridge.py (new), test_holographic_meshbridge.py (new), +holographic_unified.py (2 faculties), test_integration.py, README, NOTES_concepts.md, tour.py. + +TIER 3 OPENED with the bridge. The FWD backlog is now: Tier 1 (FWD-3/4/5/6) DONE; Tier 2 core (FWD-7 core, FWD-8, +FWD-9, FWD-10) DONE; Tier 3 FWD-11 DONE. REMAINING: FWD-7 remainder (bevel/bridge/loop-cut); ARCH items (ARCH-4 +atlas/seams -> a real FWD-3 seam; ARCH-1 StructureRecipe validator+edit-ops mirroring the Euler operators; ARCH-3 +geometry-weighted graph ops; etc.). The mesh DCC suite is now broadly complete end to end. + + +-------------------------------------------------------------------------------- +ARCH-1 -- the first §ARCH item: turn the 3-D DCC concepts INWARD on the engine's own structures. FWD-7 gave the +MESH its local invariant-preserving editors (the Euler operators: flip/split/collapse, each preserving chi + the +manifold). ARCH-1 is the exact mirror for the StructureRecipe (the one build-graph program/tree/scene all reduce +to, B7): a VALIDATOR (check well-formedness -- the recipe's is_manifold) + EDIT OPERATORS that rewrite a recipe +while preserving its meaning. + +THE PARALLEL (the point): a mesh Euler op preserves a topological invariant; a recipe edit op preserves the +REALIZED VECTOR -- for the SAME reason: it is a local rewrite that is an IDENTITY of the underlying algebra. bind +is circular convolution (commutative); bundle/superpose are sums (commutative). So: + * commute_bind -- bind(a,b)=bind(b,a) <-> flip_edge (its own inverse, preserves the invariant) + * reorder_members -- bundle(any order) is equal <-> a parameterised flip (invertible by the inverse perm) + * substitute_atom -- rename a leaf <-> a vertex-position move (structure fixed, result changes, reversible) + +WHAT SHIPPED (holographic_recipeops.py; additive; four UnifiedMind faculties): + * validate(recipe) [faculty validate_recipe] -> (ok, problems) -- every op references only EARLIER existing + results (DAG, no forward/dangling/out-of-range refs), raw indices + repeat templates in range. + * commute_bind(recipe, handle) [recipe_commute_bind] -- swap a bind's args. Vector-preserving, OWN INVERSE. + * reorder_members(recipe, handle, perm) [recipe_reorder_members] -- permute a bundle/superpose's members. + Vector-preserving, invertible by the inverse perm. + * substitute_atom(recipe, handle, new_name) [recipe_substitute_atom] -- rename an atom leaf. Validity-preserving, + result changes predictably, invertible by renaming back. + Each returns a NEW recipe (originals untouched, as the mesh operators returned new meshes). _op_index_for_handle + maps an absolute result handle to its op position (repeat produces several results, so it's not just `handle`). + +MEASURED (the bar): + * validate ACCEPTS a well-formed recipe and REJECTS a corrupted one (a forward/out-of-range reference, a bad raw + index) -- with human-readable problems. + * commute_bind + reorder_members leave the realized vector BIT-EXACT to FFT precision (1e-12) and the recipe + valid; commute_bind applied twice literally restores the op (own inverse); reorder undone by the inverse perm. + * substitute_atom CHANGES the realized vector and reverses EXACTLY by substituting the original name back. + * Edits don't mutate the original recipe; deterministic. + +KEPT NEGATIVES (loud): + * These are the VECTOR-PRESERVING / structure-preserving edits (the recipe's Euler-operator CORE). Edits that + REMOVE/RESIZE ops (flatten a nested superpose, splice out dead results) require re-indexing every downstream + handle -- the recipe analogue of the mesh face-list reindex in collapse/dissolve -- and are deferred; the + in-place edits are correct and complete on their own (as flip_edge is). + * "bit-exact" is up to FFT/float round-off (~1e-12), an algebraic identity (FP-equal not literally bit-equal) -- + the same honest caveat the bind_batch vectorization carries. + +Tests: +15 (1313 -> 1328). test_holographic_recipeops.py (+14): validate accepts/rejects (forward ref, bad raw); +commute_bind preserves vector + own inverse + rejects non-bind; reorder preserves vector + inverts + rejects +non-permutation; substitute_atom changes + reverses; edits keep validity; edits don't mutate the original; +determinism. test_integration.py (+1): the recipe editors through the mind end-to-end. Files: +holographic_recipeops.py (new), test_holographic_recipeops.py (new), holographic_unified.py (4 faculties), +test_integration.py, README, NOTES_concepts.md, tour.py. Faculty count -> 250 (round milestone). NEXT §ARCH: +ARCH-4 atlas/seams (-> a real FWD-3 seam); ARCH-3 geometry-weighted graph ops; ARCH-5 subdivision-for-structures; +ARCH-6 rig+IK-for-structures; ARCH-7 representation routing. Plus FWD-7 remainder (bevel/bridge/loop-cut). + + +-------------------------------------------------------------------------------- +ARCH-4 -- seam cutting / atlas: opening a closed surface into a disk by vertex duplication. THE FWD-3 PAYBACK. +FWD-3 (UV unwrap) shipped with a kept negative -- a CLOSED surface needs a CUT to flatten, and its only opener was +`puncture` (delete a vertex, leaving a tiny hole that unwraps badly). ARCH-4 supplies the real thing: cut along a +SEAM (an edge path) by DUPLICATING the seam's interior vertices, opening the surface into a disk that keeps ALL +its geometry. + +THE SUBTLE PART (why FWD-3 deferred it): a seam arc does NOT separate the surface, so you cannot 2-colour faces +left/right globally -- the sides are LOCAL. Fix: ORIENT the seam (v0->...->vk) and at each interior vertex +duplicate the fan on the side matching the path direction (the fan containing the face carrying directed edge +v_i->v_{i+1}). That side is defined by the single path orientation, so the duplicated side is consistent all along +the seam and the two lips line up -> a manifold. (Get this wrong -> non-manifold mess.) + +WHAT SHIPPED (holographic_meshseam.py; additive; two UnifiedMind faculties): + * cut_seam(mesh, seam) [mesh_cut_seam] -- cut open along an ordered vertex path, duplicating interior seam + vertices on a consistent side. Returns a new (open) Mesh. _components_of_incident_faces splits a vertex's + umbrella into its two fans (faces sharing a non-seam edge through the vertex); _face_with_directed_edge + picks the consistent side. + * shortest_seam(mesh, a, b) [mesh_shortest_seam] -- shortest edge path (Dijkstra), e.g. a meridian. + * _boundary_loop_count -- verify the cut made exactly one boundary (a disk). + +TOPOLOGY: cutting a closed genus-0 surface (chi=2) along a simple arc of k edges duplicates its k-1 interior +vertices and splits each of k seam edges into two -> dchi = (k-1)-k = -1: chi 2->1, a DISK. + +MEASURED (the bar): + * cut_seam(sphere, meridian) -> a DISK: chi=1, NOT closed, manifold, exactly ONE boundary loop, V grown by + (interior seam vertices = 15 for the icosphere meridian). + * ROBUST PAYBACK (always true): the cut is NON-DESTRUCTIVE -- preserves all 512 faces; the puncture DELETES 4 + faces (loses geometry). A real seam keeps the whole surface. + * DISTORTION PAYBACK (good seam): a pole-to-equator seam unwraps at 0.405 < the puncture's 0.507. + * Deterministic. + +KEPT NEGATIVE (measured, loud): SEAM CHOICE MATTERS. A FULL pole-to-pole meridian opens a valid disk but unwraps +WORSE than the puncture (0.579 > 0.507) -- it makes a long thin lune. One cut never makes a sphere unwrap WELL +(Gauss); a good atlas uses several cuts / multiple charts (the rest of ARCH-4, deferred). The win is +"non-destructive, and beats the puncture with a sensible seam", not "distortion-free". The first-draft assumed any +meridian beats the puncture -- WRONG (the full meridian doesn't); the honest finding (seam-dependent) is kept. + +Tests: +10 (1328 -> 1338). test_holographic_meshseam.py (+9): cut opens to a disk (chi=1, manifold, open); one +boundary loop; interior vertices duplicated; preserves every face; puncture deletes faces but cut doesn't; +well-chosen seam beats puncture distortion; full meridian is worse (the kept negative); shortest_seam is a valid +edge path; determinism. test_integration.py (+1): seam cutting through the mind (disk, non-destructive, beats +puncture) end-to-end. Files: holographic_meshseam.py (new), test_holographic_meshseam.py (new), +holographic_unified.py (2 faculties), test_integration.py, README, NOTES_concepts.md, tour.py. (Note: edges() +yields sorted TUPLES not frozensets -- a test-only normalisation fix, no code change.) NEXT §ARCH: ARCH-3 +(geometry-weighted graph ops), ARCH-5 (subdivision-for-structures), ARCH-6 (rig+IK-for-structures), ARCH-7 +(representation routing). Plus FWD-7 remainder (bevel/bridge/loop-cut). + + +-------------------------------------------------------------------------------- +ARCH-7 -- representation routing: the POLICY layer on top of FWD-11's mesh<->SDF<->splat bridge. FWD-11 built the +conversions; ARCH-7 decides WHEN to use them. Different operations are natural in different representations, so +route to the one that makes an operation easy, do it there, convert back. (Same shape as the decode-vs-evaluate +principle for vectors -- use the representation the operation actually fits.) + +THE FLAGSHIP: CSG (constructive solid geometry). Boolean union/intersection/difference have NO native mesh +implementation (robust mesh booleans need surface-surface intersection, never built). On an SDF they are trivial +exact FIELD ops: union=min(dA,dB), intersection=max(dA,dB), difference=max(dA,-dB). So the router takes meshes -> +SDF (mesh_to_sdf) -> combine fields -> extract back to mesh (marching tetrahedra, FWD-11). Crucially this lets a +boolean CHANGE TOPOLOGY -- two separate spheres become ONE blob when overlapping, stay TWO when not -- which a mesh +cannot do to itself; the field merges/keeps-separate automatically. + +WHAT SHIPPED (holographic_route.py; additive; four UnifiedMind faculties): + * REPRESENTATION_CAPABILITIES -- the routing table (which ops each representation supports: sdf owns + booleans/inside_test/offset, mesh owns boundary/render/subdivide, splat owns blend/scatter). + * representation_for(op) [route_representation] -- the routing decision. + * route_csg(op, A, B, res, bounds) [mesh_csg] -- the flagship boolean via SDF routing. Returns a Mesh. + * connected_components(mesh) [mesh_connected_components], mesh_volume(mesh) [mesh_volume] -- the measurements. + +MEASURED (the bar): + * the table sends booleans -> "sdf" and boundary/render -> "mesh"; "union" is explicitly NOT a mesh capability + (that is WHY routing exists). + * OVERLAPPING spheres: union merges to ONE connected component, a closed manifold (topology merged). SEPARATE + spheres: union stays TWO components (separation preserved). + * GEOMETRICALLY correct, not just topologically -- inclusion-exclusion holds to a few percent: vol(uni) 6.50 ~ + vA+vB-vInt 6.55; vA 3.82 ~ vInt+vDiff 3.77. + * Deterministic. + +KEPT NEGATIVES (loud): + * Resolution is the grid's (FWD-11 inherited): sharp intersection seams round at the cell size. Volumes converge + to the truth FROM BELOW (marching-tet under-fills) -- hence the inclusion-exclusion checks carry a few-percent + tolerance, not machine precision. + * route_csg trusts mesh_to_sdf's sign, reliable for convex-ish inputs but mis-signs deep concavities in an INPUT + mesh; the spheres are convex so the combined field is exact. A non-convex input needs a winding-number sign + (the FWD-11 fix, deferred). + * The table is a small curated policy (published strengths), not a learned cost model. + +Tests: +14 (1338 -> 1352). test_holographic_route.py (+13): table routes booleans->sdf + boundary->mesh; union not +a mesh capability; unknown op raises; overlapping union -> 1 component; union is closed manifold; separate union -> +2 components; intersection smaller than inputs; difference smaller than minuend; inclusion-exclusion for union; +intersection+difference recovers A; components of a single sphere = 1; determinism. test_integration.py (+1): CSG +routing through the mind (policy + merged-topology union + inclusion-exclusion). Files: holographic_route.py (new), +test_holographic_route.py (new), holographic_unified.py (4 faculties), test_integration.py, README, +NOTES_concepts.md, tour.py. Faculty count -> 256. NEXT §ARCH: ARCH-3 (geometry-weighted graph ops), ARCH-5 +(subdivision-for-structures), ARCH-6 (rig+IK-for-structures). Plus FWD-7 remainder (bevel/bridge/loop-cut). + + +-------------------------------------------------------------------------------- +ARCH-3 -- geometry-weighted graph operations on hypervectors: the COTANGENT LAPLACIAN, turned inward. On a mesh +(FWD-4) the cotangent Laplacian weights edges by the actual geometry (angles) and respects the shape where uniform +combinatorial weights distort it. The engine's graphs (knn_adjacency over stored vectors) are BINARY (every edge +1). The natural geometry of the hypervector world is COSINE SIMILARITY, so a similarity-WEIGHTED graph is the +cotangent analogue. + +WHAT SHIPPED (holographic_simgraph.py; additive; three UnifiedMind faculties): + * similarity_adjacency(vectors, k, weighted) [similarity_graph] -- a kNN graph; weighted=True -> each edge carries + the cosine similarity (the geometry), weighted=False -> the engine's existing BINARY kNN graph (reused verbatim). + * spectral_embedding(vectors, k, dims, weighted) [graph_spectral_embedding] -- Laplacian eigenmaps (low + eigenvectors of the weighted graph Laplacian) via holographic_spectral's graph_laplacian/laplacian_eigenbasis. + * ring_order(vectors, k, weighted) [graph_ring_order] -- recovered cyclic coordinate atan2(e2,e1) for ring points. + +MEASURED (the bar): + * POSITIVE (clean): the weighted similarity-graph eigenmap RECOVERS a ring -- recovered cyclic order tracks the + true angle to |corr|=0.998 from high-D hypervectors. The geometry-weighted op recovers intrinsic manifold + structure. + * WHERE WEIGHTING WINS: under NON-UNIFORM sampling (points bunched into arcs) the weighted graph recovers the ring + BETTER than binary (0.917 > 0.812 at seed 0; weighted wins 6/6 seeds) -- the cosine weighting corrects sampling + density, exactly as the cotangent Laplacian corrects an irregular mesh. + * weighted adjacency entries ARE the cosine similarities (varying, in (0,1]); binary entries are all 1. + * Deterministic. + +KEPT NEGATIVES (loud, measured -- the honest headline): + * Under UNIFORM sampling / well-separated data, similarity-weighting and the BINARY graph essentially TIE (ring + recovery 0.998 either way). This is a REAL difference from the mesh: a mesh's edge LENGTHS vary by orders of + magnitude so cotangent-vs-uniform differs sharply, but in high dimension the CONCENTRATION OF MEASURE makes a + kNN graph's edges nearly equal in strength, so weighting has little to correct. Geometry-weighting here helps + most under IRREGULAR SAMPLING, not universally. + * Downstream tasks the mesh weighting would help (cluster label propagation, vector denoising by graph smoothing) + showed NO weighted-over-binary gain on well-separated high-D clusters in this engine, same concentration reason + -- measured during development and kept; the module ships the operations + the regime where weighting + demonstrably helps (irregular sampling on a continuous manifold), not an overclaim that weighting always wins. + +Tests: +10 (1352 -> 1362). test_holographic_simgraph.py (+9): weighted eigenmap recovers a ring; weighted edges +carry varying similarities; binary edges all 1; adjacency symmetric; weighting wins under non-uniform sampling; +weighting ties binary under uniform sampling (kept negative); embedding shape; ring_order length; determinism. +test_integration.py (+1): geometry-weighted graph through the mind (ring recovery + weights + non-uniform win). +Files: holographic_simgraph.py (new), test_holographic_simgraph.py (new), holographic_unified.py (3 faculties), +test_integration.py, README, NOTES_concepts.md, tour.py. Faculty count -> 259. NEXT §ARCH: ARCH-5 +(subdivision-for-structures, mirrors FWD-8 inward), ARCH-6 (rig+IK-for-structures, mirrors FWD-9/10 inward). Plus +FWD-7 remainder (bevel/bridge/loop-cut). + + +-------------------------------------------------------------------------------- +ARCH-5 -- subdivision curves on hypervector sequences: FWD-8's Loop subdivision, turned inward onto a 1-manifold. +FWD-8 subdivided a MESH (2-manifold): refine (1 tri -> 4) + low-pass smooth toward a limit surface. ARCH-5 does the +same to the engine's own 1-D structure -- a SEQUENCE of hypervectors is a polyline through vector space (what the +sequence faculties encode) -- via CHAIKIN corner-cutting (the curve analogue of Loop, generator of a quadratic +B-spline limit): each edge (p_i,p_{i+1}) -> (3/4 p_i + 1/4 p_{i+1}, 1/4 p_i + 3/4 p_{i+1}), which both REFINES +(doubles the count) and SMOOTHS (corner-cutting is a low-pass filter). + +THE MESH PROPERTIES MAP ACROSS EXACTLY: + Loop faces x4/level <-> Chaikin points x2/level + Loop flat-stays-flat (affine) <-> Chaikin straight-line-of-vectors-stays-straight (affine) + Loop -> limit surface <-> Chaikin -> limit curve + Loop dihedral-spread shrinks <-> Chaikin roughness (2nd-diffs) shrinks + +WHAT SHIPPED (holographic_subdivcurve.py; additive; one UnifiedMind faculty): + * chaikin_subdivide(points, closed) -- one level of corner-cutting. + * subdivide_sequence(points, levels, closed) [subdivide_sequence] -- `levels` of Chaikin on a vector sequence; + returns the refined (M,dim) sequence. + +MEASURED (the bar): + * REFINE: open polyline n -> 2(n-1)/level ([6,10,18,34]); closed -> 2n/level ([6,12,24,48]). + * AFFINE REPRODUCTION: a straight line of vectors (linear ramp) stays ON the line to 2e-15 -- the exact analogue + of FWD-8's "flat stays flat". + * CONVERGENCE: curve length deltas [21.9,5.7,2.2,0.9,0.4] shrink (approaches a limit curve). + * LOW-PASS: a zig-zag's roughness [128,16,2,0.25,0.03] shrinks ~8x/level (corner cutting removes high freqs). + * Deterministic. + +KEPT NEGATIVE (loud): Chaikin is APPROXIMATING, not interpolating -- the limit curve cuts the original control +points' corners and does NOT pass through interior control points (nearest 0.25, not ~0). This is the EXACT mirror +of FWD-8's negative (Loop approximates -> a curved icosphere smooths to Loop's own limit, not the exact sphere). An +INTERPOLATING scheme (Dyn-Levin-Gregory 4-point) keeps the control points but is less smooth and needs >=4 points +with boundary special-casing -- the classic approximating/interpolating trade-off, deferred. Also: the open scheme +cuts the end corners too (first/last control points not preserved; an endpoint-preserving boundary rule is separate). + +Tests: +10 (1362 -> 1372). test_holographic_subdivcurve.py (+9): open/closed counts double; single-level count; +straight line stays straight; curve length converges; zig-zag roughness shrinks; approximating (control points not +interpolated); short sequence unchanged; determinism. test_integration.py (+1): subdivide_sequence through the mind +(refine + affine + low-pass). Files: holographic_subdivcurve.py (new), test_holographic_subdivcurve.py (new), +holographic_unified.py (1 faculty), test_integration.py, README, NOTES_concepts.md, tour.py. Faculty count -> 260 +(round milestone). NEXT §ARCH: ARCH-6 (rig+IK-for-structures, mirrors FWD-9/10 inward) -- the last §ARCH item. Plus +FWD-7 remainder (bevel/bridge/loop-cut). + + +-------------------------------------------------------------------------------- +ARCH-6 -- rig + IK for STRUCTURES via blendshape posing: FWD-9 (linear blend skinning) + FWD-10 (IK) turned inward. +The LAST inward mirror, and it closes the §ARCH block. A "rig" is a set of pose-TARGET structures (blendshapes) +p_1..p_m; a pose is a soft weighted blend pose(w)=normalize(sum w_i p_i). The two halves of FWD-9/10 map across: + * FORWARD = SKINNING (FWD-9): given weights, the pose is the blend -- FWD-9's soft mixture of bone transforms, + one rung up (mixing whole structures, not transforms). + * INVERSE = IK (FWD-10): given a GOAL structure, SOLVE the blend weights to reach it -- via the SAME + project_onto_constraints sweeper FWD-10 used for FABRIK. The "joint angles" are the weights; the swept + constraints are FIT-the-goal (least-squares gradient step = FABRIK's reach) + VALID-CONVEX-BLEND (simplex + projection = FABRIK's bone-length projection). Literal reuse, distinct from 3-D mesh IK (this is IK in the + engine's semantic vector space). + +WHAT SHIPPED (holographic_blendpose.py; additive; two UnifiedMind faculties): + * blend_pose(targets, weights) [blend_pose] -- forward skinning/blendshape map: normalize(sum w_i targets_i). + * solve_pose(targets, goal, iters) [solve_pose] -- IK: solve the blend weights via project_onto_constraints + ([fit, simplex_project]). Returns a valid convex blend. _simplex_project = Duchi et al. 2008 simplex projection. + +MEASURED (the bar): + * FORWARD: a one-hot weight reproduces that target exactly (1e-12); a mix leans toward its targets. + * IK REACHABLE: goal IS a known interior blend -> recovers the weights (L1 err 0.000) and the achieved pose + matches the goal (residual 1e-15). The analogue of FWD-10 hitting a reachable target exactly. + * IK UNREACHABLE: random goal outside the span -> CLOSEST valid blend (residual 18.94 <= best single target 22.37, + a GUARANTEE: the simplex it searches contains every vertex) but cannot reach (residual > 1). The analogue of + FWD-10's chain fully extending toward an out-of-reach target. + * solved weights are always a valid convex blend (w>=0, sum=1). + * Deterministic. + +CRITICAL IMPLEMENTATION NOTE (kept): the least-squares step size MUST come from the Lipschitz constant (largest +eigenvalue of the Gram P P^T, ~dim for random targets); a fixed step diverges and the simplex projection collapses +to a vertex (the bug found in the probe). mu = 1/L fixes it -> exact recovery. + +KEPT NEGATIVES (loud): + * The IK CANNOT reach a goal outside the targets' convex blend -- returns the closest valid blend (the honest + analogue of FWD-10's unreachable target). Reaching arbitrary goals needs a richer rig (more targets), not a + better solver. + * Returns A best convex blend, not THE only one: linearly-dependent targets -> non-unique weights (the POSE is + still optimal, the weights just aren't identifiable) -- the same "a-solution-not-the-solution" caveat as FWD-10. + * Forward map is a blend in the AMBIENT vector space (FWD-9's linear-blend analogue), not a nonlinear pose + manifold -- the same scope as linear blend skinning (whose own negative was the candy-wrapper collapse). + +Tests: +10 (1372 -> 1382). test_holographic_blendpose.py (+9): one-hot blend is that target; mix leans to targets; +IK recovers a reachable blend; reachable pose matches goal; unreachable is closest valid blend; unreachable cannot +reach; solved weights valid simplex; simplex projection lands on simplex; determinism. test_integration.py (+1): +blendshape posing through the mind (forward + reachable IK + closest-blend). Files: holographic_blendpose.py (new), +test_holographic_blendpose.py (new), holographic_unified.py (2 faculties), test_integration.py, README, +NOTES_concepts.md, tour.py. Faculty count -> 262. + +*** §ARCH BLOCK COMPLETE: ARCH-1 (recipe Euler ops), ARCH-2 (delta protocol, prior session), ARCH-3 (geometry- +weighted graph ops), ARCH-4 (real seam), ARCH-5 (subdivision curves), ARCH-6 (rig+IK for structures), ARCH-7 +(representation routing) all shipped. Each turned a piece of the FWD mesh pipeline inward onto the engine's own +structures. *** NEXT: FWD-7 modeler remainder (bevel/bridge/loop-cut) is the main remaining FWD thread. + + +-------------------------------------------------------------------------------- +FWD-7 REMAINDER -- bevel, bridge, loop-cut: the three modeler verbs FWD-7 deferred because they need vertex +DUPLICATION or edge-loop TRACING (FWD-7 shipped the face-list-rewrite verbs extrude/inset/dissolve). This ships +them, reusing the vertex-fan/umbrella logic from the ARCH-4 seam and the unused-vertex compaction (reindex) from +dissolve. + +WHAT SHIPPED (holographic_meshverbs2.py; additive; three UnifiedMind faculties): + * bevel_vertex(mesh, vertex, ratio) [mesh_bevel_vertex] -- chamfer a corner: pull each incident edge back toward + its neighbour by ratio, chamfer every incident face, cap the hole with a new face. Needs the cyclic neighbour + order (the umbrella) + compaction of the removed corner vertex. + * bridge_loops(verts, loop_a, loop_b, closed) [mesh_bridge] -- join two equal-length ordered vertex loops with a + band of quads (build a tube between two openings). + * loop_cut(mesh, start_face, start_edge) [mesh_loop_cut] -- trace the perpendicular quad loop (enter a quad + through one edge, leave through the OPPOSITE edge, cross to the neighbour) and split every crossed quad in two. + +MEASURED (the bar): + * BEVEL a cube corner (degree 3): closed manifold, chi PRESERVED (2); the 3 incident quads become PENTAGONS and a + TRIANGULAR cap appears (face sizes [3,4,4,4,5,5,5]); V = 8-1+3 (corner removed, 3 new). New verts sit on the + incident edges at the ratio. + * BRIDGE two squares -> an open tube: 4 quads, chi=0, exactly TWO boundary loops, manifold. + * LOOP-CUT a cube: closed manifold, chi PRESERVED (2), +4 faces (the ring crosses 4 quads). LOOP-CUT a grid(3,3): + chi PRESERVED (1), +3 faces (the open strip crosses 3 quads). + * Deterministic. + +TWO BUGS FOUND IN THE PROBE (kept as notes): (1) loop-cut first split quads after reordering the cycle to start at +an arbitrary vertex -> inconsistent winding across the strip -> non-manifold ("directed edge appears twice"). FIX: +split using the quad's OWN native cyclic order (v0,v1,v2,v3 from the entering-edge position) so adjacent cells wind +oppositely on the shared cut edge. (2) bevel left the removed corner vertex ORPHANED in the array -> chi wrong (3 +not 2). FIX: _compact drops unused vertices and reindexes (the dissolve/seam reindex). + +KEPT NEGATIVES (loud): + * BEVEL is the VERTEX bevel (chamfer a corner). The EDGE bevel (widen an edge into a chamfer face, splitting BOTH + endpoints' fans) is the harder two-sided split, deferred -- the same fan-consistency the seam solved for one + path, here needed on both sides. ratio must be in (0,1); boundary/non-manifold vertices out of scope. + * BRIDGE requires two EQUAL-LENGTH, ALREADY-ALIGNED loops (caller supplies the correspondence); resampling/matching + unequal loops (the general bridge) is deferred. + * LOOP-CUT needs QUADS (the opposite-edge trace is undefined on triangles); the trace stops at a boundary (open + cut) or when it returns to the start (closed ring). + +Tests: +12 (1382 -> 1394). test_holographic_meshverbs2.py (+11): bevel closed-manifold + chi preserved; bevel +pentagons+cap; bevel vertex count; bevel new verts near corner; bridge open tube; bridge two boundary loops; bridge +unequal loops raises; loop-cut box chi+4faces; loop-cut grid chi+3faces; loop-cut on triangle raises; determinism. +test_integration.py (+1): all three verbs through the mind. Files: holographic_meshverbs2.py (new), +test_holographic_meshverbs2.py (new), holographic_unified.py (3 faculties), test_integration.py, README, +NOTES_concepts.md, tour.py. Faculty count -> 265. + +*** With this, the FWD direct-modeling verb set is complete: extrude/inset/dissolve (FWD-7) + bevel/bridge/loop-cut +(remainder). The broad FWD DCC pipeline + the full §ARCH inward-mirror block are both done. *** + + +-------------------------------------------------------------------------------- +HOLOGRAPHIC SCENE-GRAPH ALGEBRA -- the capstone that joins the FWD mesh kernel to the ARCH-1 recipe algebra. A +scene graph (leaves are meshes, edges are transforms) read TWO ways at once: as GEOMETRY (instance + merge) and as +STRUCTURE (encode to a StructureRecipe). The point is that the two views are CONSISTENT -- VSA is geometry, and the +scene is one object wearing both costumes. + +WHAT SHIPPED (holographic_scenegraph.py; additive; seven UnifiedMind faculties): + * SceneNode(transform, mesh, children) [scene_graph] -- a node: a 4x4 transform, an optional leaf mesh, optional + children. + * identity/translation/scaling/rotation/compose_transforms [scene_translation/scene_scaling/scene_rotation/ + scene_compose_transforms] -- 4x4 transform builders (rotation is Rodrigues). + * flatten_scene(node) [scene_flatten] -- the GEOMETRY view: instance every leaf through its accumulated transform + (parent transforms composed down the graph) and MERGE into one Mesh. + * scene_to_recipe(node, dim, seed) [scene_to_recipe] -- the STRUCTURE view: encode as a StructureRecipe + (transforms BOUND to content via bind, siblings BUNDLED), realising to one hypervector. Leaf/transform atom + names are content hashes (hashlib). + +THE CONSISTENCY THEOREM (the unification, measured): swapping two siblings leaves the flattened GEOMETRY identical +(same sorted vertices, same face count -- a mesh merge is commutative) AND the realised VECTOR identical (bundle is +commutative). So a structural edit from ARCH-1 (recipe_reorder_members on the sibling bundle) is a no-op on the +geometry too -- the two representations agree. The scene recipe is a WELL-FORMED recipe (passes ARCH-1's validate), +so the recipe Euler operators apply to scenes. + +MEASURED (the bar): INSTANCING -- a scene of 2 cubes flattens to one mesh V=16 F=12, the +x instance lands with its +centroid at its translation; NESTED transforms compose to +2 (parent then child); CONSISTENCY -- sibling swap +leaves geometry AND vector identical; distinct transforms -> distinct structure vectors; the scene is a valid +recipe; deterministic (same scene -> byte-identical mesh and vector). + +KEPT NEGATIVES (loud): + * flatten_scene INSTANCES and concatenates -- it does NOT weld coincident vertices or boolean-merge overlapping + geometry (that is mesh_csg / ARCH-7's job); two touching cubes flatten to two components, not one solid. This is + scene assembly, not constructive solid geometry. + * scene_to_recipe encodes the scene's STRUCTURE (which transform holds which content), not its geometry -- the + mesh hash distinguishes meshes but the vector is a structural index, and recovering geometry is scene_flatten's + job, not the vector's. + * the encoding bundles siblings, so (decode ceiling) a node with very many children loses per-child + recoverability from the root vector -- the same capacity cliff every bundle carries; wide scenes index + structurally but are not meant to be decoded child-by-child from the root. + +Tests: +14 (1394 -> 1408). test_holographic_scenegraph.py (+13): transform builders (translation/rotation/scaling/ +compose); instancing merges; instance lands at translation; nested transforms compose; identity node; sibling-swap +geometry identical; sibling-swap vector identical; valid recipe; distinct scenes -> distinct vectors; determinism. +test_integration.py (+1): the full scene-graph algebra through the mind. Files: holographic_scenegraph.py (new), +test_holographic_scenegraph.py (new), holographic_unified.py (7 faculties), test_integration.py, README, +NOTES_concepts.md, tour.py. Faculty count -> 272. + +*** This is the geometry capstone: the FWD mesh pipeline + the §ARCH inward mirror now meet in one object -- a scene +that is simultaneously a pile of triangles and a composed hypervector, with the two provably consistent. The +standing thesis (VSA is geometry) made concrete: the scene graph IS the recipe. *** + + +-------------------------------------------------------------------------------- +QEM DECIMATION -- the quadric error metric (Garland-Heckbert, SIGGRAPH 1997), the one genuinely-missing piece of a +principled mesh simplifier. From the geometry->stack backlog sweep: the engine already had the guarded +collapse_edge (eulerops, the link-condition refusal made operational), the heapq priority-descent with +deterministic ties (HoloForest/Dijkstra), the curvature read-out (meshcurvature), and the greedy-by-error loop +shape (matching pursuit) -- ONLY the cost function (the quadric) was absent. This supplies it and wires it to the +shipped collapse. + +WHAT SHIPPED (holographic_meshqem.py; additive; two UnifiedMind faculties): + * vertex_quadrics(mesh) -- per-vertex 4x4 error quadrics Q_v = sum over incident faces of (plane plane^T), + plane = [n, -n.p]; v^T Q_v v is the summed squared distance from v to its incident planes. + * contraction_target(Q, p_i, p_j) -- the optimal merged position argmin v^T Q v and its cost; singular 3x3 -> + best of {midpoint, endpoints}. + * qem_decimate(mesh, target_faces) [mesh_qem_decimate] -- greedily collapse the lowest-cost edge (deterministic + ties by vertex index) via the guarded collapse_edge, ACCUMULATING quadrics through each collapse (the survivor + inherits Q_keep + Q_remove), until <= target_faces. + * surface_deviation(mesh_a, mesh_b) [mesh_surface_deviation] -- (mean, max) point-to-surface distance (a + decimation quality metric; uses the closed-form point-to-triangle distance). + +WHY IT BELONGS IN THIS ENGINE (the reverse thesis, concrete): a quadric is Q_v = sum of (plane plane^T) -- an +OUTER-PRODUCT ACCUMULATION = a BUNDLE of plane constraints in matrix form, with the collapse cost read out as a +quadratic. That is bind/bundle/readout in a different costume. So QEM IS a general "merge the two items whose +combined representation loses the least" operator, which is exactly reverse item R2 (prototype compaction in the +creature -- combine redundant prototypes instead of evicting), and the same shape as the splat merge / codebook +merge. Build it once for meshes; it is the merge operator everywhere. (R2 is a future wire on this.) + +MEASURED (the bar): icosphere V66 F128 -> QEM F64, closed manifold, chi PRESERVED (2); QEM BEATS a naive +shortest-edge->midpoint baseline on MEAN point-to-surface error (~1.8x) AND dramatically on MAX error (~3x -- naive +spikes where it collapses a feature edge); a vertex's own quadric vanishes at the vertex (it lies on its incident +planes); the cost is never negative; deterministic. + +KEPT NEGATIVES (loud): + * QEM minimizes squared distance to incident PLANES, not the true surface or any invariant -- so on a sphere the + optimal points sit slightly OFF-RADIUS (|r-1| a touch worse than the chord-midpoint baseline) while being + CLOSER to the actual surface (point-to-surface, the honest metric, is much better, esp. max). The plane metric + is the right one; radius fidelity is not what it optimizes. + * CLOSED meshes are in scope; OPEN-mesh boundary preservation (the standard high-weight perpendicular-plane + penalty per boundary edge) is deferred. + * The loop recomputes edge costs each pass (clear + correct); the incremental heap-with-lazy-deletion that makes + QEM near-linear is the standard perf upgrade, deferred (the panel's "delegate the heavy grind" call) -- this is + the readable, correct version for moderate meshes. + * collapse_edge REFUSES manifold-breaking collapses (link condition); the decimator tries the next-cheapest edge + and HALTS if no safe collapse remains -- so it may stop above target_faces. A true mesh property, operational. + +Tests: +11 (1408 -> 1419). test_holographic_meshqem.py (+10): quadric vanishes at its vertex; quadric symmetric; +cost non-negative; singular -> midpoint fallback; decimate preserves closed manifold + chi; reaches target; QEM +beats naive mean error; QEM beats naive max error; surface_deviation zero for identical mesh; determinism. +test_integration.py (+1): QEM through the mind, beating naive. Files: holographic_meshqem.py (new), +test_holographic_meshqem.py (new), holographic_unified.py (2 faculties), test_integration.py, README, +NOTES_concepts.md, tour.py. Faculty count -> 274. + +*** First item off the geometry->stack backlog: the sweep found the engine had every part of a decimator but the +quadric; this is the quadric, and (per the reverse thesis) it is the general error-minimizing MERGE operator -- the +same Sigma-nn^T-and-read-the-cost shape the creature's prototype compaction (R2) wants. *** + + +-------------------------------------------------------------------------------- +OCTAHEDRAL NORMAL ENCODING -- quantize a unit vector on its MANIFOLD, not its ambient bits (Cigolle, Donow, +Evangelakos, Mara, McGuire, Meyer, JCGT 2014). From the geometry->stack backlog item A2: the shipped quantizer +(int8/quant='rd') quantizes a value's ambient bits, but a unit normal has only 2 DOF (it lives on S^2), so +quantizing 3 x/y/z components wastes a third of the budget on a constrained coordinate. The octahedral map projects +onto the octahedron (L1) and unfolds the lower hemisphere into a 2D square -- 2 numbers, bounded error, bits on the +intrinsic DOF. + +WHAT SHIPPED (holographic_octnormal.py; additive; two UnifiedMind faculties): + * oct_encode(normals) / oct_decode(uv) -- the continuous bijection S^2 <-> [-1,1]^2 (exact to float precision). + * oct_quantize(normals, bits) [oct_encode_normals] -- integer codes (N,2) in [0, 2^bits). + * oct_dequantize(codes, bits) [oct_decode_normals] -- unit normals back. + * _sign_nz -- sign that returns +1 at zero (np.sign gives 0, which breaks the fold at the poles, e.g. [0,0,-1]). + +WHY IT BELONGS (reverse thesis): manifold quantization made concrete -- "spend bits on the surface the data lives +on" -- which is the engine's binary-quantization-distorts-the-geometry negative turned into a method. The same +PRINCIPLE is reverse item R3: the FHRR phasor memory is unit-magnitude complex (S^1) and a normalized hypervector +lives on a high-D sphere, so both want their intrinsic coordinate quantized (for a phasor that analog is the PHASE +ANGLE -- one number, not two). Octahedral is the concrete S^2 instance; R3 is the principle carried to the phasor +memory (a future wire). + +MEASURED (the bar): continuous round-trip EXACT (max ~1e-6 deg, a bijection); 8-bit quantized round-trip small + +BOUNDED (max 0.93 deg, mean 0.34); at an EQUAL 16-bit budget octahedral (8+8) BEATS naive x/y/z (5+5+6, +renormalized) on mean angular error 0.34 vs 1.20 deg (~3.5x) -- the manifold-quantization win; axis-aligned + the +z<0 pole survive (the fold edge case, fixed by _sign_nz); decode outputs unit vectors; deterministic. + +KEPT NEGATIVES (loud): + * At EQUAL bits-PER-COMPONENT naive xyz is more accurate -- because it spends 50% more bits (3 comps vs 2). The + octahedral win is a STORAGE win (same accuracy in 2 numbers naive needs ~2.5-3 for), stated so the per-component + numbers aren't misread. + * Octahedral is specific to S^2 (3-D unit vectors). It does NOT generalize verbatim to S^1 (FHRR phasors) or a + high-D sphere -- those use the SAME PRINCIPLE with a different intrinsic coordinate. The literal map is for + normals; R3 is the principle, not this function. + * The fold has measure-zero seams (the octahedron edges, z=0) where the (u,v) representation is non-unique; points + there still decode to a valid unit vector -- the standard harmless oct caveat. + +Tests: +9 (1419 -> 1428). test_holographic_octnormal.py (+8): continuous round-trip exact; axis-aligned + poles +roundtrip; 8-bit bounded error; codes in range; decode outputs unit vectors; more bits -> lower error; oct beats +naive at equal budget; determinism. test_integration.py (+1): octahedral through the mind, beating naive. Files: +holographic_octnormal.py (new), test_holographic_octnormal.py (new), holographic_unified.py (2 faculties), +test_integration.py, README, NOTES_concepts.md, tour.py. Faculty count -> 276. + +*** Second item off the geometry->stack backlog (item A2, paired with QEM). The concrete S^2 case of manifold +quantization -- the same "quantize the intrinsic DOF" principle reverse item R3 wants for the FHRR phasor memory. *** + + +-------------------------------------------------------------------------------- +SPECTRAL BANDWIDTH + A SINGULARITY CROSS-CHECK -- the genuinely-new parts of the fractal-optics backlog's +"fractal-dimension/bandwidth probe" (item 2). The DE-DUP discipline applied to the engine itself: the audit found +fractal DIMENSION is already shipped (box-counting + R/S Hurst in holographic_fractal; the fractal_dimension / +self_affinity faculties), so this ships ONLY the two missing pieces the review named, not another dimension. + +WHAT SHIPPED (holographic_bandwidth.py; additive; two UnifiedMind faculties): + * spectral_bandwidth(x, energy_fraction) [spectral_bandwidth] -- the fraction of Nyquist holding that energy + fraction; the number that drives a band-limited encoder's bandwidth knob (the next item). Small for band-limited + content, near 1 for broadband. GENUINELY NEW (the review's "the probe's real job is bandwidth measurement"). + * spectral_dimension(x) -- the power-spectrum-slope dimension D=(5-gamma)/2 (Berry & Klein), a fast estimator used + as a cross-check term (NOT the engine's primary dimension). + * fractal_confidence(x) [fractal_confidence] -- (d_spectral, d_increment, agree): two INDEPENDENT slope estimators + and whether they agree -- the singularity flag. The shipped single-estimator dimension silently returns a wrong + number for a step/tone; this catches it. + +MEASURED (the bar): bandwidth smooth sinusoid 0.0007 << white noise 0.947 of Nyquist; rougher fBm (lower Hurst) -> +more bandwidth; on clean fBm of known H the two slope estimators AGREE and bracket D=2-H (spectral 1.70 / increment +1.65 at H=0.3); a STEP (isolated singularity) -> spectral 2.03 / increment 1.50 DISAGREE -> flag fires; a pure tone +-> disagree -> flag fires; deterministic. + +A MEASURED FINDING (kept): the cross-check uses spectral-slope vs increment-variance, NOT the shipped R/S Hurst -- +because R/S reads a DIFFERENT number on the same clean fBm (it is a range statistic weighting coarse/low-frequency +trend-dominated scales, while the slope methods fit the whole power law). They measure different things, so R/S is a +poor naive co-validator here. The honest cross-check is slope-vs-slope. (R/S stays correct for what it ships for -- +series persistence.) An instance of the backlog discipline: check the live code, build only the delta, and report +exactly where two methods legitimately disagree. + +KEPT NEGATIVES (loud): + * spectral_bandwidth is an ENERGY rolloff: a fractal's front-loaded 1/f^b energy can read a small bandwidth even + though its self-similar detail extends higher -- band-limiting to the energy-bandwidth keeps the bulk, discards + the fine detail (the fundamental fractal trade; superoscillation is the standing proof it can't be cheated for + free). Honest about fidelity-for-a-budget, not lossless bandwidth. + * the power-spectrum-slope dimension is the one FOOLED by singularities -- it exists here only as a cross-check + term, never the engine's reported dimension; trust a dimension only when agree. + * 1-D signals; higher-D the slope relation is approximate, and images already use the shipped box-counting. + +Tests: +10 (1428 -> 1438). test_holographic_bandwidth.py (+9): bandwidth separates smooth/broadband; bandwidth in +[0,1]; rougher fBm -> more bandwidth; spectral D recovers fBm; increment D recovers fBm; cross-check agrees on fBm; +cross-check flags a step; cross-check flags a pure tone; determinism. test_integration.py (+1): bandwidth + +cross-check through the mind. Files: holographic_bandwidth.py (new), test_holographic_bandwidth.py (new), +holographic_unified.py (2 faculties), test_integration.py, README, NOTES_concepts.md, tour.py. Faculty count -> 278. + +*** First fractal-optics backlog item. The de-dup lesson again: the engine already had fractal dimension three ways, +so the real work was the BANDWIDTH driver (for the next item, the band-limited-encoding faculty) and the cross-check +the single-estimator dimension lacked. Build only the delta. *** + + +-------------------------------------------------------------------------------- +AUTO-BANDWIDTH KDE VIA THE ENCODER -- the disciplined form of the fractal-optics backlog's "band-limited-encoding +faculty" (Item N). A LIVE AUDIT of the encoder reshaped the ask, and the slog produced several kept negatives before +landing on what actually delivers. + +THE AUDIT (what the review's premise got wrong about the live code): + * The SINC kernel's bandwidth is NOT tunable -- its width is fixed at scale=1/(hi-lo); the `bandwidth` parameter + only affects the RBF phases. So "tune the sinc ideal filter to Nyquist" does not apply; only RBF bandwidth is + selectable. KEPT NEGATIVE. + * The encoder is a SCALAR encoder, not a function encoder -- reconstructing an oscillatory function by bundling + weighted samples + Nadaraya-Watson collapses to the mean and does not benefit from bandwidth tuning. KEPT + NEGATIVE (the failed approach; measured RMSE ~0.7 = predicting the mean). + * The encoder's DOCUMENTED use is the RBF kernel as a KDE ("a bundle of encoded points reads as a proper KDE"), + and THERE the bandwidth IS the band-limit with a real optimum (U-shaped error). The faculty lands here. + +WHAT SHIPPED (holographic_kde.py; additive; two UnifiedMind faculties): + * kde_bandwidth(samples, lo, hi, method) [kde_bandwidth] -- RBF bandwidth by 'lcv' (leave-one-out likelihood, + robust) or 'silverman' (cheap fallback). + * density_estimate(samples, lo, hi, query, dim, seed, method) [density_estimate] -- KDE via the encoder (bundle of + encoded samples, density ~ bundle . encode(x)), bandwidth auto-selected, output normalized to integrate ~1. + Returns (density_at_query, bandwidth). + +THE KEY BUG FOUND + KEPT: LCV REQUIRES a NORMALIZED kernel. The encoder's kernel is unnormalized (its integral grows +with width), so naive LCV collapses to the WIDEST bandwidth (measured: it picked bw=2, the floor). The selection +normalizes the Gaussian per candidate (1/(std*sqrt(2pi))) and then LCV works -- landing near the ground-truth +optimum on both bimodal (bw 39 vs optimum 40) and unimodal (bw 22 vs optimum 20) densities. The encoder is still +used for the actual estimate; only the selection normalizes. + +MEASURED (the bar): bimodal density -- LCV bw 39 near optimum 40, shape RMSE 0.17 BEATS the fixed default (bw 1.8) +1.16 by 6.8x and Silverman 0.45; estimate correlation 0.99 with truth; unimodal -- LCV near optimum, beats default +~7x; the density integrates to ~1; a too-small dim (16 vs 1024) gives worse correlation at the same bandwidth (the +capacity negative); deterministic. + +KEPT NEGATIVES (loud): + * SINC bandwidth is not tunable in the shipped encoder (only RBF) -- the review's sinc-ideal-filter knob does not + apply. + * LCV requires normalized kernels (the bug above) -- naive LCV on the encoder's unnormalized kernel collapses. + * Silverman's rule (fallback) over-smooths MULTIMODAL data (~2.6x vs LCV's ~6.8x) -- the standard caveat. + * Bandwidth selection fixes the SMOOTHING match, NOT capacity: a too-small dim cannot be rescued by any bandwidth. + * Function reconstruction (vs density estimation) is NOT this encoder's job (the failed Nadaraya-Watson approach). + +Tests: +10 (1438 -> 1448). test_holographic_kde.py (+9): LCV beats default bimodal; LCV near optimum bimodal; LCV +near optimum unimodal; estimate correlates with truth; Silverman beats default but worse than LCV; density +integrates to ~1; capacity negative (small dim worse); silverman bandwidth is a number; determinism. +test_integration.py (+1): auto-bandwidth KDE through the mind. Files: holographic_kde.py (new), +test_holographic_kde.py (new), holographic_unified.py (2 faculties), test_integration.py, README, NOTES_concepts.md, +tour.py. Faculty count -> 280. + +*** Second fractal-optics backlog item. The audit reshaped the review's over-promise (tune the sinc to Nyquist) +into what the shipped encoder actually supports: auto-bandwidth KDE, where the bandwidth IS the band-limit and LCV +matches it to the data 6.8x better than the default. The kept negatives (sinc not tunable, NW collapses, LCV needs +normalization) are the audit working. *** + + +-------------------------------------------------------------------------------- +SCREEN-SPACE-ERROR LOD POLICY -- the geometry->stack backlog's "geometric screen-space-error policy." The piece that +turns QEM decimation + surface_deviation (both shipped) into an actual DECISION: which simplification to show. + +THE REVERSE-THESIS CONNECTION (why it belongs here): this is the engine's own error-budget RESOLUTION SELECTION +carried to meshes. coarse_to_fine refines a query only until an error budget is met; multires_pyramid keeps a signal +at several scales; the equidistribution rule places resolution where needed. select_lod is that rule for geometry -- +the coarsest level of a decimation chain whose error, projected to the screen, meets a pixel budget. The principle +is the one the engine already uses for signals and queries; only the domain (meshes) and the budget unit (pixels) +are new. + +WHAT SHIPPED (holographic_lod.py; additive; two UnifiedMind faculties): + * build_lod_chain(mesh, targets) [mesh_lod_chain] -- QEM-decimate to coarser levels at face-count fractions, + measuring each level's surface deviation (mean, max) from the ORIGINAL. Returns fine->coarse LODLevel records; + level 0 is the original (zero error). + * screen_space_error(world_error, distance, screen_height_px, fov_rad) -- project a world error to screen pixels: + sse = world_error * screen_height / (2 * distance * tan(fov/2)). + * select_lod(chain, distance, pixel_threshold, ...) [mesh_select_lod] -- index of the COARSEST level whose max + screen-space error stays under the pixel budget (the cheapest mesh that looks right at that distance). + +MEASURED (the bar): chain off an icosphere F[128, 64, 32, 16] with max deviation [0.0, 0.072, 0.105, 0.174] +(monotone -- fewer faces, growing error); screen error falls with distance; LOD selection by distance [0,0,0,2,3] +across 2..200 units (full detail up close, F16 far away) -- monotone coarsening; the choice is TIGHT (at d=50 it +picks F32 at 1.97px while F16 would breach the 2px budget); a tighter pixel threshold or higher screen resolution +forces a finer level; deterministic. + +KEPT NEGATIVES (loud): + * the error driving the policy is GEOMETRIC surface deviation (a Hausdorff-style distance), not a perceptual or + silhouette metric -- a coarse mesh can be within the pixel budget on average yet show a visible silhouette + break. The policy is exactly as good as surface_deviation is. + * the projection ignores foreshortening and screen position (the standard conservative LOD estimate, not a + per-pixel bound). + * the chain inherits QEM's limits (closed meshes, boundary handling) -- this selects among levels, it does not + improve them. + +Tests: +13 (1448 -> 1461). test_holographic_lod.py (+12): chain has several levels; first level is the original +zero-error; face count strictly decreases; deviation only grows; screen error falls with distance; screen error +scales with resolution; LOD coarsens with distance; full detail up close / coarser far; selection is tight; tighter +threshold never coarser; higher resolution never coarser; determinism. test_integration.py (+1): LOD policy through +the mind. Files: holographic_lod.py (new), test_holographic_lod.py (new), holographic_unified.py (2 faculties), +test_integration.py, README, NOTES_concepts.md, tour.py. Faculty count -> 282. + +*** geometry->stack backlog item, completing the QEM decimation story (decimate -> measure -> SELECT). The reverse +thesis again: a geometric LOD policy is the engine's error-budget resolution selection (coarse_to_fine) in the mesh +domain -- same rule, different units. *** + + +-------------------------------------------------------------------------------- +BINDING-STABILITY REGIME TEST -- the fractal-optics backlog's "band-limit-preservation regime test" (Trefethen +transient-growth / pseudospectra spirit), grounded in the engine's actual bind. The investigation measured all three +relevant operations on the real substrate; the Trefethen framing came up empty and the real story is a LINEAR one. + +WHAT THE INVESTIGATION FOUND (all measured, in the self-test): + * LINEAR ops preserve the band-limit -- bind, bundle, permute all map a white spectrum to a white spectrum + (high-frequency-energy fraction ~0.5 throughout). No spectral concentration. + * The CLEANUP shows NO transient growth -- a pure HIGH-FREQUENCY perturbation of a stored atom, iterated through + the dense-associative (modern-Hopfield) cleanup, contracts MONOTONICALLY to zero (one step at usable beta). The + non-normal transient amplification Trefethen's lens looks for does not appear. + * So the real stability axis is a LINEAR property of the binding KEY: its SPECTRAL FLATNESS. unbind(bind(x,k),k) + returns x convolved with |K|^2, equal to x only when |K|=1 everywhere -- a UNITARY key (flatness 1.0). A random + key (flatness ~0.5) DISTORTS, and the distortion compounds catastrophically over a chain. + +WHAT SHIPPED (holographic_flatness.py; additive; two UnifiedMind faculties): + * spectral_flatness(v) [spectral_flatness] -- Wiener entropy (geometric/arithmetic mean of the power spectrum), + (0,1]; 1.0 = unitary, distortion-free key. + * binding_distortion(key, seed, trials) -- the measured single-round bind/unbind distortion (ground truth flatness + predicts). + * binding_stability(v, tol) [binding_stability] -- {'flatness', 'distortion', 'stable'}: the regime diagnostic for + a key. + +THE DE-DUP (what is NOT new): the stable regime itself is already shipped -- unitary_vector mints flat-spectrum +atoms, and holographic_array / holographic_assembly already use them "for exact unbind." What was missing, and is the +genuinely-new contribution, is the DIAGNOSTIC: measuring where any vector sits on the stability spectrum, and the +regime test confirming flatness predicts distortion. Answers "is this key safe to bind/unbind repeatedly?" + +MEASURED (the bar): flatness unitary 1.000 vs random 0.594; a unitary key is EXACT (chain-64 bind/unbind error < +1e-9), a random key distorts ~0.93 and compounds; flatness PREDICTS distortion -- across keys blended unitary->random +the flatness falls [1.0, 0.95, 0.81, 0.57, 0.3] and distortion rises [0.0, 0.33, 0.75, 1.13, 1.47] monotonically; +linear ops preserve a white spectrum; the cleanup contracts monotonically; deterministic. + +KEPT NEGATIVES (loud): + * the stable regime is NOT new (unitary_vector exists); this adds the MEASUREMENT. And unitarity is a mint CHOICE, + not a free default -- the engine's own record notes a starved-maze bootstrap that went to zero under unitary + atoms (their flatness removes a redundancy some paths rely on). Flatness tells you the binding cost, not that + unitary is always right. + * the Trefethen transient-growth framing, taken literally, came up EMPTY -- the honest result is a linear-stability + story (key flatness), not a non-normal-dynamics one. Reported as found. + * flatness governs binding (convolution) specifically; bundle capacity and cleanup confusability are separate axes. + +Tests: +10 (1461 -> 1471). test_holographic_flatness.py (+9): flatness separates unitary/random; unitary key exact; +random key lossy; unitary chain stays exact; flatness predicts distortion monotonically; linear ops preserve white +spectrum; cleanup contracts monotonically (no transient growth); binding stability report; determinism. +test_integration.py (+1): binding stability through the mind. Files: holographic_flatness.py (new), +test_holographic_flatness.py (new), holographic_unified.py (2 faculties), test_integration.py, README, +NOTES_concepts.md, tour.py. Faculty count -> 284. + +*** Third fractal-optics backlog item. The Trefethen lens looked for transient growth and found none -- a clean +negative -- so the honest deliverable is the LINEAR stability diagnostic the data actually pointed at: spectral +flatness predicts binding distortion, with unitary keys (already shipped) as the flatness=1 exact regime. *** + + +-------------------------------------------------------------------------------- +SPLAT PRUNE / MERGE + A QUALITY-BUDGET LOD CHAIN -- the geometry->stack backlog's "splat prune/merge + exporter." +The splat-domain twin of the mesh LOD policy: reduce an existing splat set while holding quality, and pick a level +for a budget -- there the budget was screen-space pixels, here it is reconstruction PSNR. + +THE KEY MOVE: each splat renders as amp * gaussian and the engine's gaussians are UNIT-NORM, so a splat's +reconstruction energy is exactly amp^2 -- "which splats matter" is "which have the largest |amp|". Drop the rest, +then one joint amplitude REFIT (splat_refit, the closed-form lstsq already in the engine) lets the survivors absorb +the overlap the removed ones carried. Contribution-ranked prune + refit degrades gracefully and dominates naive +pruning by a wide margin. + +WHAT SHIPPED (holographic_splatprune.py; additive; four UnifiedMind faculties): + * splat_prune(splats, target, keep) [splat_prune] -- keep the top-`keep` splats by |amp|, refit. + * splat_merge(splats, target, radius) [splat_merge] -- merge splats closer than radius (amplitude-weighted centre + and scale, summed amplitude), refit; reduces count. + * splat_lod_chain(splats, target, keeps) [splat_lod_chain] -- prune to each count, measuring PSNR; returns + fine->coarse (splats, count, psnr). + * select_splat_lod(chain, min_psnr) [splat_select_lod] -- the fewest-splat level meeting the PSNR budget. + +MEASURED (the bar): full 60 splats 44.5 dB; prune to 20 -- contribution 38.3 dB DOMINATES random 18.3 / worst (keep +smallest) 16.6 (a ~20 dB margin); LOD chain counts [60,40,20,10,5] -> PSNR [44.5,43.7,38.3,32.0,29.0] (graceful, +monotone); merge to 29 splats 39.9 dB (bounded loss); budget-30 keeps 10 splats, budget-43 keeps 40 (tighter budget +-> more splats); deterministic. + +KEPT NEGATIVES (loud): + * NO .ply / .spz exporter. Those are 3D-Gaussian-splatting formats (per-splat position, scale, rotation, opacity, + spherical-harmonic colour); the engine's splats are 2-D field primitives (cy, cx, amp, sigma) -- the format does + not fit the representation, so shipping it would be a mislabelled stub. Stated, not faked. + * prune/merge operate on the ISOTROPIC splat format (splat_fit's output); the anisotropic splats (aniso_fit) carry + a covariance and have their own optimiser; this does not prune them. + * |amp| ranking is a proxy for true contribution when splats OVERLAP (energies not independent); the refit + compensates but a jointly-removable overlapping pair is not detected as such -- good enough, not optimal. + * merge is lossy by construction (one Gaussian cannot equal two); a large radius over a busy region loses real + structure. + +Tests: +12 (1471 -> 1483). test_holographic_splatprune.py (+11): contribution prune beats random; beats keeping +smallest; keep-all returns full; prune reduces count; LOD chain counts decrease; LOD PSNR degrades gracefully; merge +reduces count; merge loss bounded; tighter budget keeps more; selection meets budget; determinism. +test_integration.py (+1): splat prune/LOD through the mind. Files: holographic_splatprune.py (new), +test_holographic_splatprune.py (new), holographic_unified.py (4 faculties), test_integration.py, README, +NOTES_concepts.md, tour.py. Faculty count -> 288. + +*** geometry->stack backlog item. The splat twin of the mesh LOD policy (decimate->measure->select becomes +prune->measure->select), same error-budget resolution selection in a different domain. The .ply/.spz exporter was +DECLINED honestly -- the format is for 3D Gaussians and the engine's splats are 2-D field primitives. *** + + +-------------------------------------------------------------------------------- +SCENE COMPONENT DELTA -- the geometry->stack backlog's reverse item R6 ("cluster/scene delta"). The investigation +measured it on the real scene-graph and found the honest scope, which is the point worth recording: + +THE SAVING IS AUTOMATIC. scene_to_recipe names every component (mesh, transform) by CONTENT HASH, so two scenes that +share a subtree already share the identical atom; stored in any content-addressed table they dedup for FREE -- +measured 3.86x fewer stored components across a base + 8 variants each changing one of four subtrees (54 -> 14). There +is NO new delta ALGEBRA to invent; content-addressing already does the sharing (the same reason a content-addressed +blob store dedups a repo). This is the thin-item outcome flagged before the probe -- reported plainly. + +WHAT SHIPPED (holographic_scenedelta.py; additive; two UnifiedMind faculties) -- only the genuinely-useful, +NOT-automatic operations: + * scene_delta(base, variant) [scene_delta] -- {'added', 'removed'} content-hashed component ids: the explicit DIFF, + so a variant is TRANSMITTED as its delta (send base once, then small deltas) rather than re-sent whole. + * apply_scene_delta(base_components, delta) -- rebuild the variant's component set from base + delta (exact). + * scene_components(scene) -- the content-hashed component-id set (the handle sharing keys on). + * scene_dedup_saving(scenes) [scene_dedup_saving] -- {'naive', 'unique', 'saving_x'}: quantify the automatic + sharing. + +MEASURED (the bar): a one-subtree change -> delta 1+1 vs full 6 components; base+delta rebuilds the variant exactly; +an identical scene -> empty delta; dedup across 9 scenes saves 3.86x (54 -> 14 components); deterministic. + +KEPT NEGATIVES (loud): + * the dedup saving is AUTOMATIC from content-addressed atoms, NOT a contribution of this module -- it exposes and + measures it and adds the transmittable diff. Stated, not dressed up as a new mechanism. (This was the thin item + flagged before probing; the probe confirmed it.) + * the delta is over COMPONENTS (the heavy mesh/transform atoms); the scene TREE wiring is rebuilt by the recipe, so + a delta that only re-wires shared components reads as an empty component delta though the scene changed. + * sharing requires BIT-IDENTICAL components (the hash is exact) -- a near-but-not-identical mesh does not dedup; + this is why geometric quantization (making near-identical things identical) matters upstream. + +Tests: +9 (1483 -> 1492). test_holographic_scenedelta.py (+8): one-subtree change is small; reconstruction exact; +identical scene empty delta; dedup saving above 1x; dedup accounting consistent; variants share most components; +apply-delta round trip with added+removed; determinism. test_integration.py (+1): scene delta through the mind. +Files: holographic_scenedelta.py (new), test_holographic_scenedelta.py (new), holographic_unified.py (2 faculties), +test_integration.py, README, NOTES_concepts.md, tour.py. Faculty count -> 290. + +*** Reverse item R6, probed honestly. The reverse thesis held -- a scene delta IS a content-addressed component diff +-- but the dedup turned out AUTOMATIC (content-hashing already shares), so the only genuinely-new deliverables are the +explicit diff (for transmission) and the saving measurement. Shipped those; recorded the rest as a kept finding. This +was the thin item flagged in advance; the measurement confirmed it. *** + + +-------------------------------------------------------------------------------- +RT-V -- OCCLUSION RECALL: alpha-compositing carried to bundle readout. The first of the two high-value reverse +transfers from the 3DGS-concepts sweep, and the one that targets the engine's OLDEST standing negative -- the linear +bundle capacity cliff (separation collapses ~1/sqrt(count) as atoms pile up). + +THE TRANSFER (as above, so below): 3DGS composites front-to-back with a running transmittance so each pixel SATURATES +after the front few splats -- the tail is occluded, not summed. holostuff's bundle is the opposite (a linear, order- +free sum that washes out). The fix the graphics side already found, transferred to recall: sort atoms by relevance to +the cue, accumulate front-to-back, each atom contributing only what the front has not explained. Concretely this is +matching pursuit as the readout -- pick the most-relevant atom, record its share, SUBTRACT its explained part (the +transmittance), repeat. + +WHAT SHIPPED (holographic_occlusion.py; additive; one UnifiedMind faculty): + * occlusion_recall(cue, codebook, m, min_share) [occlusion_recall] -- recover the components present in `cue` (a + bundle of codebook atoms) as (index, weight) pairs in front-to-back (descending-relevance) order. `m` fixes the + count; else stop below `min_share`. + +MEASURED (the bar): HIGH LOAD M=50 over a 200-atom codebook -- occlusion holds F1 1.000 while the linear / softmax / +TopK top-m readouts all wash out TOGETHER to 0.914 (they re-rank the same cosines, so for component recovery they are +identical -- the doc's point, confirmed); LOW LOAD M=4 -- occlusion TIES linear at 1.000 (the kept negative); weighted +recovery error 0.005 with the heaviest atom recovered FIRST; deterministic. + +WHY DISTINCT FROM THE HOPFIELD READOUTS IT RESEMBLES: softmax (z = V^T softmax(beta V q)) and TopK also saturate, but +they are GLOBAL and ORDER-FREE. Occlusion's SEQUENTIAL transmittance -- a later atom sees less BECAUSE an earlier one +absorbed it -- is the new ingredient, and the measurement separates them cleanly (occlusion 1.000 vs softmax/TopK +0.914 at high load). + +KEPT NEGATIVES (loud): + * at LOW load / few well-separated atoms it TIES plain linear recall and hard-NN -- the win is a PURE HIGH-LOAD + phenomenon (the regime occlusion was invented for), exactly as the Hopfield update ties hard-NN on single-item + identity (B1). + * this IS matching pursuit / OMP-style recovery in VSA clothing -- stated plainly. The contribution is the transfer + (front-to-back saturating readout breaking the cliff) and the measured separation from the order-free readouts. + * SORTING/ITERATION is the price (each step scans the codebook O(N*D)); on a large store the nearest-first step is + what HoloForest provides sub-linearly -- this module does the dense scan. + * THRESHOLD stopping (min_share) slightly OVER-recovers at very high load (noise-floor picks); fixing the count with + `m` is exact. + +Tests: +9 (1492 -> 1501). test_holographic_occlusion.py (+8): high-load beats linear; softmax/TopK reduce to linear +for recovery; low-load ties linear; recovers all present at high load; weighted recovery; front-to-back heaviest +first; threshold stopping ~right count; determinism. test_integration.py (+1): occlusion recall through the mind. +Files: holographic_occlusion.py (new), test_holographic_occlusion.py (new), holographic_unified.py (1 faculty), +test_integration.py, README, NOTES_concepts.md, tour.py. Faculty count -> 291. + +*** RT-V from the 3DGS->Gaussian-data-structure sweep. The alpha-compositing transfer breaks the linear-bundle cliff +for multi-component recall, holding perfect F1 where the order-free readouts wash out -- with the low-load tie kept +loud, and the matching-pursuit identity acknowledged honestly. The high-value rung of the two worth climbing. *** + + +-------------------------------------------------------------------------------- +RT-VI -- CONTEXT-DEPENDENT MEANING IN A HARMONIC BASIS. The second of the two high-value reverse transfers from the +3DGS-concepts sweep, and the DEEPEST substrate fit: it points the engine's own FFT/phase/FPE basis at MEANING instead +of geometry. + +THE TRANSFER (as above, so below): a 3DGS splat's COLOUR is a function of view direction, expanded in spherical +harmonics -- DC term = base colour, higher degrees = smooth view-dependent variation. holostuff's FHRR/FPE already +uses PHASE = a point on the circle = a direction, so an atom whose MEANING is a function of a context angle is native: +represent content(theta) in a CIRCULAR-harmonic (Fourier) basis. The DC term is the context-FREE meaning (today's +fixed atom, exactly); higher harmonics encode how the meaning shifts with context. Reading at a context angle is the +harmonic sum -- the analog of unbinding with an FPE-encoded role(theta). This gives CONTEXT-CONDITIONED / POLYSEMOUS +atoms (a word's sense under different contexts; a filler that means different things under different roles). + +WHAT SHIPPED (holographic_harmonic.py; additive; three UnifiedMind faculties): + * harmonic_atom(thetas, meanings, n_harmonics) [harmonic_atom] -- fit a context-conditioned atom: the circular- + harmonic coefficients (least squares) of the meaning function sampled at (context angle, meaning) pairs. K keeps + the DC plus K-1 harmonics (2K-1 coefficient vectors). + * harmonic_decode(atom, theta) [harmonic_decode] -- the meaning at context angle theta (the harmonic sum). + * harmonic_dc(atom) [harmonic_dc] -- the DC (degree-0), context-free meaning -- exactly the plain fixed atom. + +MEASURED (the bar): POLYSEMY -- 3 distinct senses at 3 contexts each recovered at their context (cosine >0.999), a +between-context decodes to a blend (cos 0.69/0.68); DEGREE-0 FALLBACK -- a context-free atom decodes EXACTLY (<1e-10) +from the DC alone (the backward-compatible reduction to the plain atom, the way beta->inf reduces Hopfield to +hard-NN); SMOOTH WIN -- a meaning function band-limited to B=3 reconstructs EXACTLY at K=B+1 (7 vectors, err 1.9e-14) +for ANY context, BEATING a per-context nearest-neighbor store at 24 vectors (err 4.13); DEGENERATE TRAP -- a +non-smooth meaning function is NOT captured by a few harmonics (err 10.5, worsens with K); deterministic. + +WHY SUBSTRATE-ALIGNED, NOT BOLTED ON: spherical harmonics are Fourier-on-the-sphere; the core bind is FFT-on-a-domain, +FHRR is phasors, and the manifold module already picks harmonic bases for ring/torus topologies. The engine's own +native basis pointed at appearance/meaning -- the same move RT-IV1 made with anisotropy, one concept over. + +KEPT NEGATIVES (loud): + * for CONTEXT-FREE atoms the harmonic expansion spends coefficients for no gain -- the DC term suffices, so it MUST + tie the plain atom there (and does, by construction). The win exists ONLY where the context variation is real AND + smooth. + * if the variation is NOT smooth it degenerates to storing every context (measured) -- the bare-codebook + degenerate-sampler trap from B10, in this basis. + * CIRCULAR harmonics only (a 1-D context angle, the FPE phase case); full SPHERICAL harmonics over a 2-D direction + is the natural extension and is NOT implemented. + * the fit is least squares: 2K-1 >= samples interpolates exactly, else smooths/aliases -- choose K against the + sample count and the expected smoothness. + +Tests: +9 (1501 -> 1510). test_holographic_harmonic.py (+8): polysemy recovery; between-context blend; degree-0 +fallback exact; DC is the context-free mean; smooth exact at K=B+1; beats per-context NN; non-smooth degenerate trap; +determinism. test_integration.py (+1): context atom through the mind (polysemy + degree-0 fallback). Files: +holographic_harmonic.py (new), test_holographic_harmonic.py (new), holographic_unified.py (3 faculties), +test_integration.py, README, NOTES_concepts.md, tour.py. Faculty count -> 294. + +*** RT-VI from the 3DGS->Gaussian-data-structure sweep. Context-conditioned / polysemous atoms on the engine's own +FFT/phase/FPE substrate -- distinct meaning per context where the variation is smooth, with the DC term as the exact +backward-compatible fallback and the "store every context" degenerate trap kept loud. The deepest rung of the two +worth climbing; both now climbed. *** + + +-------------------------------------------------------------------------------- +CLONE-VS-SPLIT DENSITY CONTROL -- scale-aware splat densification (the first of the two REFINEMENTS from the 3DGS sweep). + +THE REFINEMENT: 3DGS densifies where error is high but DISTINGUISHES two moves by the splat's scale -- CLONE (high +error at a SMALL splat: duplicate-and-nudge to COVER an under-served region) vs SPLIT (high error at a WIDE splat: +subdivide into two NARROWER splats to RESOLVE fine structure it smears). The engine's existing splat_densify +(`densify_fit`, staged residual placement) adds capacity where error is, but is SCALE-BLIND -- it never asks cover vs +resolve. This adds exactly that distinction, refining an EXISTING splat set. + +DE-DUP CATCH (the discipline working): the first wiring collided -- `splat_densify` ALREADY EXISTS (the coarse-to-fine +densify_fit). The collision was a NAME clash, not a functional duplicate (the existing one places fresh splats on the +residual from scratch; this refines an existing set with the scale-aware cover-vs-resolve decision). Renamed the new +faculty to `splat_clone_split`; both now coexist as complements. + +WHAT SHIPPED (holographic_splatdensify.py; additive; one UnifiedMind faculty splat_clone_split): + * clone_splat(splat, residual, shape) -- the COVER primitive: a same-scale splat at the residual peak in the + splat's footprint (original kept). + * split_splat(splat, residual, shape) -- the RESOLVE primitive: two narrower (sigma/1.6) splats at the two largest + residual peaks (original removed). + * clone_split_densify(splats, target, n_densify, scale_thresh) -- rank splats by footprint residual error; for the + highest-error ones CLONE if narrow (< scale_thresh) else SPLIT. scale_thresh defaults to the median sigma. + +MEASURED (the bar -- and the WRONG move can be worse than nothing): COVER (ridge fit by one small splat) -- clone +0.0065 beats split 0.0086, and split is WORSE than the 0.0080 baseline (subdividing the small splat loses coverage); +RESOLVE (twin peaks smeared by one wide splat) -- split 0.0006 beats clone 0.0020 decisively; MIXED (ridge + twin +peaks) -- scale-aware 0.00704 beats always-clone 0.00799 (misses the peaks) and always-split 0.00917 (hurts the +ridge) at a fixed splat budget. Each blind strategy handles only one error type; the scale rule does the right move +for each. Deterministic. + +KEPT NEGATIVES (loud): + * REFINES splat_densify's from-scratch placement -- the "add capacity where error is high" half was shipped; the + new part is the scale-aware COVER-vs-RESOLVE decision on an existing set, and the measurement that the wrong move + can be worse than nothing. + * ISOTROPIC splats (the (cy,cx,amp,sigma) domain) -- scale = sigma directly; anisotropic per-axis cover-vs-resolve + is the natural extension, not implemented. + * the scale threshold is a HEURISTIC (median sigma); on a SINGLE-SCALE target the distinction is moot (the win + needs mixed scales). + * split positions are the two largest residual peaks in the footprint (a deterministic stand-in for 3DGS's sampling + the original Gaussian) -- adequate for two-peak resolution, not general multi-modal placement. + +Tests: +7 (1510 -> 1517). test_holographic_splatdensify.py (+6): clone wins cover; split worse than baseline on a +small splat; split wins resolve; scale-aware beats both blind; split removes original / clone keeps it (narrower vs +same scale); determinism. test_integration.py (+1): splat_clone_split through the mind beats both blind on a mixed +target. Files: holographic_splatdensify.py (new), test_holographic_splatdensify.py (new), holographic_unified.py +(1 faculty, name-collision resolved), test_integration.py, README, NOTES_concepts.md, tour.py. Faculty count -> 295. + +*** Clone-vs-split refinement from the 3DGS sweep. Sharpens WHERE densification capacity goes -- cover an under-served +region (clone a small splat) vs resolve fine structure (split a wide one) -- beating both scale-blind strategies, with +the de-dup catch (it complements, not duplicates, the existing splat_densify) on the record. *** + + +-------------------------------------------------------------------------------- +MCMC BIRTH-DEATH RELOCATION -- conserve capacity instead of dropping it (the second REFINEMENT from the 3DGS sweep, +and the LAST item of that sweep). + +THE REFINEMENT: 3DGS-as-MCMC (Kheradmand et al. 2024) replaces heuristic prune with a birth-death move -- a DEAD +(low-opacity) Gaussian is RELOCATED to a high-density region rather than dropped, so a fixed budget is never wasted on +dead samples. holostuff's bounded memory does the opposite: evict-rarest DELETES the lowest-count prototype (the +creature's `memory_cap` path, "forget the rarest"). Eviction DROPS capacity; birth-death CONSERVES it -- move the +dead atom to an under-represented region. Ties into the B10 generative-denoising sampler (birth-death IS an MCMC +move, the discrete kin of running the cleanup backwards from noise). + +DE-DUP AUDIT (the discipline): confirmed the eviction is a pure DROP (creature line 657: find argmin count, np.delete) +and no relocate-vs-drop logic exists anywhere -- birth-death is genuinely absent. This faculty is additive; the +creature's eviction is left unchanged. + +WHAT SHIPPED (holographic_relocate.py; additive; one UnifiedMind faculty splat_relocate): + * birth_death_relocate(splats, target, dead_frac) -- find the DEAD splats (|amplitude| below dead_frac of the + largest) and RELOCATE each to the current residual peak (the most under-represented region), subtracting after + each so successive relocations find distinct peaks. The splat COUNT is conserved (moved, not removed). + +MEASURED (the bar): budget 12 splats, 7 dead -- RELOCATE to residual peaks 0.00167 BEATS DROP 0.00730 (~4.4x lower +MSE; eviction shrinks the budget to 5 and wastes it) and BEATS RANDOM relocation 0.00723 (the principled target -- the +under-represented region -- is what wins, not the move alone); count conserved 12->12; no-dead is a no-op; +deterministic. + +KEPT NEGATIVES (loud): + * SUCCESSOR to evict-rarest -- the DROP was already in the box (the creature's bounded memory); the new part is + CONSERVING capacity by relocating a dead atom to an under-represented region instead of deleting it. + * ISOTROPIC splats (the "Gaussians-as-samples" structure). The same drop-vs-relocate choice applies to the + creature's prototype eviction and any bounded store -- that broader wiring is noted, not done (eviction unchanged). + * the relocation TARGET is the residual peak (a deterministic stand-in for 3DGS-MCMC's opacity-weighted sampling) -- + redistributes toward high RESIDUAL (the under-served region), the right target for COVERAGE, the honest + simplification of the MCMC move. + * NO dead splats -> NO-OP (nothing to redistribute); the win exists only when capacity is being wasted. + +Tests: +7 (1517 -> 1524). test_holographic_relocate.py (+6): relocate beats drop; residual target beats random; count +conserved; no-dead no-op; relocate improves reconstruction; determinism. test_integration.py (+1): splat_relocate +through the mind beats drop at a conserved budget. Files: holographic_relocate.py (new), test_holographic_relocate.py +(new), holographic_unified.py (1 faculty), test_integration.py, README, NOTES_concepts.md, tour.py. Faculty count +-> 296. + +*** Birth-death relocation from the 3DGS sweep -- the successor to evict-rarest. Conserve capacity by relocating a +dead atom to an under-represented region instead of dropping it (~4.4x better than eviction at a fixed budget), with +the principled-target-beats-random result and the de-dup audit (the drop was the only thing in the box) on the record. +This CLOSES the 3DGS-concepts sweep: RT-V (occlusion), RT-VI (harmonic context), clone-vs-split, and birth-death -- +all four reverse transfers climbed. *** + + +-------------------------------------------------------------------------------- +SPEED-1 -- GRAM-CACHED OCCLUSION RECALL (Batch-OMP). The first fix from the occlusion-speed panel review. Occlusion +recall (RT-V) broke the bundle capacity cliff but cost ~170x the linear readout -- a sequential matching pursuit, +O(M*N*D), M rescans of the dictionary. The panel found this is the speed problem of greedy sparse recovery, solved +three ways in compressed sensing (cache the Gram = the D factor; approximate the search = the N factor; batch the +selection = the M factor). This ships the D-factor one, the highest-value and the one the user's RAM hunch pointed at. + +THE FIX (Rubinstein-Zibulevsky-Elad 2008, Batch-OMP): occlusion is PLAIN matching pursuit, so rather than recomputing +cb @ residual each step (O(N*D)) it maintains the correlation vector alpha = cb @ residual and updates it through one +Gram COLUMN per pick: alpha -= share*G[:,j] (O(N)) -- the D factor leaves the inner loop. G = cb @ cb.T is a CACHED +precompute, computed once and reused across cues (the engine's normal case: one fixed vocabulary, many recalls). + +WHAT SHIPPED (holographic_occlusion.py extended; additive, backward-compatible; one new UnifiedMind faculty): + * build_gram(codebook) [build_occlusion_gram] -- the cached Gram matrix G = cb @ cb.T. + * occlusion_recall(..., gram=G) -- the fast path; gram=None keeps the original rescan, bit-for-bit unchanged. + +MEASURED: the Gram-cached path recovers IDENTICAL atoms in IDENTICAL order (it is exact, not approximate), with +weights matching the rescan to MACHINE EPSILON (~1e-16) across every tested regime (D 256..2048, M up to 300, both +fixed-count and threshold-stop modes). Timed ~12x faster at D=512 and ~23x at D=1024 -- the speedup grows with D +because the per-step cost drops from O(N*D) to O(N). That takes occlusion from ~170x slower than linear to ~7x, exact. + +KEPT NEGATIVES (loud): + * the Gram costs O(N^2) memory and an O(N^2*D) one-time precompute -- it pays when the codebook is REUSED across + cues (it is), not for a one-shot recall against a throwaway dictionary. Without gram=, the original O(M*N*D) + rescan runs unchanged. + * the Gram path is the same recurrence REASSOCIATED through G -- identical atoms/order in every tested regime, but + weights differ by ~1e-16 (so exact-tuple == fails; compare indices). A true knife-edge argmax tie could in + principle differ (the bind_batch class of issue) -- did not manifest anywhere tested. + * this is the D-factor fix only. The N-factor (HoloForest sublinear selection, approximate) and M-factor (batch + selection a la CoSaMP/IHT, gradient-powered) remain on the backlog (PANEL_occlusion_speed_backlog.md). + +Tests: +6 (1524 -> 1530). test_holographic_occlusion.py (+5): build_gram; Gram path identical atoms/order; weights +match to epsilon; threshold-stop matches; gram=None backward compatible. test_integration.py (+1): Gram fast path +through the mind recovers identical atoms. Files: holographic_occlusion.py (extended), test_holographic_occlusion.py, +holographic_unified.py (1 faculty + occlusion_recall gram param), test_integration.py, README, NOTES_concepts.md, +tour.py. Faculty count -> 297. + +*** SPEED-1 from the occlusion-speed panel review. The RAM hunch made concrete: cache the Gram, update correlations +through a column instead of rescanning -- exact (identical recovery, weights to 1e-16), ~23x faster at D=1024, taking +occlusion from ~170x slower than linear to ~7x. The D-factor fix; N- and M-factor fixes (and the gradient-optimizer +unlock) on the backlog. *** + + +-------------------------------------------------------------------------------- +RAM-1 -- GRAM WORKING-SET CACHE (the RAM hunch, infrastructure). The companion to SPEED-1. SPEED-1 made occlusion +recall ~23x faster by passing a cached Gram, but the caller had to hold it; RAM-1 makes the Gram DURABLE -- a +vocabulary queried many times pays the O(N^2 D) precompute ONCE and the second recall is a zero-precompute hit, with +the caller needing nothing but cache=True. This is the Gram-specific realization of the cache-layer working-set the +engine has been growing toward (the general working-set faculty -- promoting ReflexCache -- remains its own backlog). + +WHAT SHIPPED (holographic_occlusion.py extended; additive; mind gains a cache flag, not a new faculty): + * GramCache(max_entries=4) -- a bounded working-set cache of codebook Grams. Keyed by codebook OBJECT IDENTITY (id, + O(1), no per-call hashing of the large codebook) and GC-SAFE via a weakref callback (a collected codebook's entry + is dropped, so an id is never reused stale); LRU-bounded. Methods: gram(codebook), clear(), len(), hits/misses. + * UnifiedMind.occlusion_recall(..., cache=True) -- the mind keeps a lazy GramCache and auto-builds/reuses the Gram. + +MEASURED: first cache=True call builds + caches (1 miss); the second call with the same codebook is a HIT (no rebuild); +recovery through the cache is IDENTICAL to the explicit-gram fast path and to the rescan; the cache stays LRU-bounded; +the weakref callback drops an entry once its codebook is garbage-collected (asserted). Deterministic. + +DESIGN NOTE (why id-keying, not content-hashing): hashing a large codebook every call (O(N D)) would erode SPEED-1's +win on the fast path, and the mind's own caches key by cheap identities (generation/shape/n) rather than content +hashes (which the engine reserves for small scene atoms). id-keying is O(1); weakref makes it GC-safe so an id can +never be reused for a stale Gram. + +KEPT NEGATIVES (loud): + * the cache assumes codebooks are IMMUTABLE (the engine's norm -- a vocabulary is built once). If a codebook is + mutated IN PLACE while keeping the same object, the cached Gram goes stale (identity unchanged, contents not); + call .clear() or pass gram= explicitly in that unusual case. + * each cached Gram is O(N^2) memory x max_entries -- the cache is bounded for exactly this reason. + * this is the Gram-specific working-set; the general cache-layer faculty (the six-item backlog) is broader and + unbuilt. + +Tests: +6 (1530 -> 1536). test_holographic_occlusion.py (+5): cache hit reuses same object; identical recovery +through the cache; LRU bound; clear; weakref GC invalidation. test_integration.py (+1): cache=True reuses the Gram +through the mind (hit on the second call, identical recovery). Files: holographic_occlusion.py (extended), +test_holographic_occlusion.py, holographic_unified.py (occlusion_recall cache param + lazy _gram_cache), +test_integration.py, README, NOTES_concepts.md, tour.py. + +*** RAM-1 from the occlusion-speed panel review. The RAM hunch finished: the Gram is now durable -- build once, reuse +across cues, zero precompute on the second call -- via a bounded, GC-safe, id-keyed working-set cache. SPEED-1 + RAM-1 +together: the D-factor fix, cached. Remaining: GRAD-2 (the general optimizer), then the M-factor (batch selection) and +N-factor (HoloForest selection) fixes. *** + + +-------------------------------------------------------------------------------- +GRAD-2 -- A GENERAL GRADIENT-DESCENT OPTIMIZER (the gradients hunch, made first-class). The 3D-Gaussian-splatting work +brought a real optimizer into the engine: the anisotropic splat fit (holographic_splat._aniso_optimize) runs Adam with +HAND-DERIVED analytic gradients -- gradient descent, no autodiff, inside the NumPy-only rule -- and the cache module +already carried finite-difference gradients (gradient_cache_fd). Both halves of a general gradient-descent capability +were in the box but SILOED: the optimizer woven into the splat-specific gradients, the FD gradient specialized to +field-maps-at-anchors. GRAD-2 promotes them to ONE reusable faculty, so "gradients on the fly" is first-class for the +whole engine. (The splat module's embedded Adam is left UNCHANGED -- specialized for speed; this is the general twin +beside it, the same update rule extracted.) It is also the prerequisite the occlusion-speed panel flagged for IHT -- +the gradient-native sparse-recovery member (GRAD-1) is a gradient step plus a threshold, wanting exactly this +optimizer underneath. + +WHAT SHIPPED (holographic_optimize.py; +2 mind faculties): + * fd_gradient(f, x, eps=1e-5) -- the central finite-difference gradient of a scalar f: R^n -> R at x (2*n + evaluations, perturbing a copy one coordinate at a time). The general scalar-loss companion to the engine's + field-map FD helper. + * optimize(loss, x0, grad=None, steps, lr, b1, b2, eps, tol, patience, min_steps, fd_eps, stats) -- minimize + loss(x) from x0 by Adam (the exact bias-corrected update the splat fit uses, generalized). Analytic grad(x) where + supplied (fast); finite-difference fallback where not. Optional convergence-gated early stop; stats={} reads the + step count and loss trajectory. Returns the optimized x. + +MEASURED (selftest + 8 wrapper tests + 1 integration): CONVEX quadratic -> its minimum (off 1.1e-9); LEAST-SQUARES -> +the lstsq solution (off 5.2e-11); the FD fallback reaches the SAME minimum as the analytic-gradient run, and +fd_gradient matches a known analytic gradient to 8e-11; NON-CONVEX Rosenbrock -> ~[0.99, 0.98] near (1,1) in 8000 +steps; convergence-gated early stop reports fewer steps than the budget; fd_gradient does not mutate its input; +deterministic (Adam + central FD are RNG-free given x0). Through the mind: least-squares -> lstsq, FD fallback -> the +quadratic min, fd_gradient matches analytic. + +KEPT NEGATIVES (loud): + * NO AUTODIFF (the constraint): supply an analytic grad for speed; the FD fallback costs 2*n loss evaluations PER + STEP -- fine for small n, expensive for large n. + * general gradient descent inherits its limits -- a poor lr diverges or crawls, and on a non-convex loss it finds a + LOCAL minimum from the given start (Rosenbrock gets close, not exact, in a fixed budget). A workhorse, not a + global solver. + * the splat module keeps its OWN embedded Adam (the splat gradients inlined for speed); this is the general + extraction beside it, not a replacement. Refactoring the splat fit to call through here is a separate, optional + step. + +Tests: +9 (1536 -> 1545). test_holographic_optimize.py (+8): convex quadratic; least-squares; FD fallback matches the +analytic run; fd_gradient matches analytic; fd_gradient preserves its input; Rosenbrock close; early stop fewer steps; +deterministic. test_integration.py (+1): the optimizer through the mind (least-squares -> lstsq, FD fallback, mind +fd_gradient). Files: holographic_optimize.py, test_holographic_optimize.py, holographic_unified.py (optimize + +fd_gradient faculties, near gradient_cache), test_integration.py, README, NOTES_concepts.md, tour.py. + +*** GRAD-2 from the occlusion-speed panel review. The gradients hunch finished: the splat fit's hand-derived-gradient +Adam, plus the cache module's finite differences, are now ONE engine-wide capability -- minimize any scalar loss, +gradients on the fly, no autodiff. Next: GRAD-1 (IHT recovery built ON this optimizer -- the gradient-native member of +the sparse-recovery family), then the M-factor (batch selection, CoSaMP/SP) and N-factor (HoloForest selection). *** + + +-------------------------------------------------------------------------------- +GRAD-1 -- ITERATIVE HARD THRESHOLDING RECOVERY (the gradient-native sparse-recovery member, built on GRAD-2). The +engine had two ways to recover the components of a bundle cue = sum_i w_i * codebook[i]: the LINEAR readout (one-shot +correlations + top-m, washes out at load) and OCCLUSION recall (GREEDY matching pursuit -- take the most-relevant +atom, subtract, repeat, never revisiting a pick). GRAD-1 adds the third: PROJECTED GRADIENT DESCENT -- a gradient step +on the reconstruction loss, then PROJECT onto the K-sparse set (keep the K largest coefficients), ITERATED, so a +coefficient dropped at one step can RETURN. That support revision is the point: greedy MP cannot undo an early wrong +pick; IHT can. + +BUILT ON GRAD-2: the gradient step is exactly the descent GRAD-2's optimize() generalized -- the loss is +0.5*||cue - c@codebook||^2, gradient -codebook@(cue - c@codebook), analytic and cheap. With NO threshold (K=N) IHT +reduces to plain gradient descent on that loss = the LEAST-SQUARES solution (matched to ~1e-11), the same point +optimize() finds. The hard-threshold projection is the ONE thing that turns the optimizer into a sparse recovery +method. This is the panel's flagged use of the 3DGS gradient machinery for recovery, made literal. + +WHAT SHIPPED (holographic_iht.py; +1 mind faculty iht_recall): + * hard_threshold(c, K) -- the K-sparse projection H_K (keep the K largest |c|, zero the rest; K>=len is identity). + * iht_recall(cue, codebook, K, steps=300, mu=None, tol=1e-12) -- IHT recovery, returns (index, weight) descending + by |weight| (occlusion_recall's signature, for a head-to-head). mu defaults to 1/||codebook||_2^2 (1/Lipschitz, + the standard descent-guaranteeing IHT step). + +MEASURED -- IHT vs occlusion (greedy MP) vs linear as dictionary COHERENCE rises (M=12, N=200, D=512, 12 seeds): + * INCOHERENT (random dictionary): IHT F1 1.000 TIES occlusion 1.000; linear lags (0.958). + * The CROSSOVER is the honest finding -- NEITHER dominates: + - MILD coherence: greedy occlusion is BETTER (0.96 vs IHT 0.87) -- when the cue still points cleanly at the + true atoms, subtract-and-move wins and IHT's iterations spread energy onto correlated decoys. + - HIGH coherence: IHT PULLS AHEAD (0.71 vs occlusion 0.54) -- greedy MP's early wrong picks on a coherent + dictionary become UNRECOVERABLE; IHT keeps revising its support and corrects them. The classic + matching-pursuit-vs-IHT result, reproduced. + * K=N bridge: IHT with no threshold matches numpy lstsq to ~1e-11 -- the reduction to gradient descent, confirming + the GRAD-2 connection. + +KEPT NEGATIVES (loud): + * NOT a universal win over occlusion: at LOW-MILD dictionary coherence greedy matching pursuit recovers a BETTER + support. IHT is the COHERENT-regime method, not a strict upgrade -- the crossover is real and on the record. + * needs two knobs occlusion does not -- the sparsity K and the step size mu (a bad mu crawls or overshoots) -- plus + an iteration budget. + * projected gradient descent, so it finds the K-sparse stationary point reachable from the zero start, not a + certified global optimum. + +Tests: +7 (1545 -> 1552). test_holographic_iht.py (+6): ties occlusion incoherent; beats occlusion coherent; K=N +reduces to lstsq; hard_threshold keeps the K largest; returns descending by magnitude; deterministic. +test_integration.py (+1): IHT recall through the mind (perfect incoherent, beats occlusion coherent). Files: +holographic_iht.py, test_holographic_iht.py, holographic_unified.py (iht_recall faculty after build_occlusion_gram), +test_integration.py, README, NOTES_concepts.md, tour.py. + +*** GRAD-1 from the occlusion-speed panel review -- the gradient-native member, built ON GRAD-2. The three recovery +routes now sit side by side: linear (one-shot), occlusion (greedy MP), IHT (projected gradient descent). The 3DGS +gradient machinery serves recovery, not just fitting -- and the honest crossover (greedy wins at low coherence, IHT at +high) is measured. Remaining occlusion-speed items: SPEED-3 (batch selection, CoSaMP/Subspace Pursuit -- the M-factor) +and SPEED-2 (HoloForest-accelerated selection -- the N-factor, approximate, ship last). *** + + +-------------------------------------------------------------------------------- +SPEED-3 -- CoSaMP BATCH-SELECTION RECOVERY (the strongest recovery-family member; the M-factor). The recovery family +had three members: LINEAR (one-shot correlations + top-m), OCCLUSION (greedy matching pursuit, one atom/step), and IHT +(projected gradient descent). SPEED-3 adds the fourth and strongest: CoSaMP (Compressive Sampling Matching Pursuit, +Needell-Tropp 2009) does BATCH selection with a LEAST-SQUARES solve each round -- identify the 2K atoms most correlated +with the residual, MERGE with the current support, solve least-squares over that merged set (the optimal coefficients), +PRUNE to the K largest, repeat. The LS solve is the difference: it gets exact coefficients and corrects errors the +greedy and gradient methods cannot, converging in a handful of rounds instead of M sequential picks. It is the +M-factor companion to SPEED-1 (which removed the D factor by caching the Gram): ~2-3 ROUNDS, not M passes. + +WHAT SHIPPED (holographic_cosamp.py; +1 mind faculty cosamp_recall): + * cosamp_recall(cue, codebook, K, iters=15, tol=1e-10, stats=None) -- CoSaMP recovery; returns (index, weight) + descending by |weight| (occlusion_recall / iht_recall signature). stats={} reads stats['rounds']. + +MEASURED -- CoSaMP vs IHT vs occlusion vs linear (M=12, N=200, D=512, 12 seeds) as dictionary COHERENCE rises: + * CoSaMP recovers PERFECTLY at EVERY coherence tested -- F1 1.000 at coh 0.0/0.5/1.0/1.5 and on up to 8.0 -- while + occlusion falls to 0.54 and IHT to 0.71. The least-squares-over-merged-support disambiguates correlated atoms the + greedy/gradient methods get stuck on. + * Converges in ~2-3 ROUNDS (vs occlusion's M=12 sequential picks) -- the M-factor win. + * Coefficients are EXACT: weight RMSE ~8e-16 (the LS solve) vs occlusion's ~0.069 (greedy subtraction accumulates + coefficient error). + +THE HONEST CLIFF (kept negative): CoSaMP is NOT magic -- it falls off at the fundamental sparse-recovery PHASE +TRANSITION, when the load M approaches the dimension D (the problem becomes underdetermined and NO method can recover). +Measured at D=128: F1 1.000 at M/D=0.16, 0.83 at M/D=0.31, ~0.57-0.59 once M/D exceeds ~0.5. Recovery lives below +roughly M < D/3. + +THE COST (kept negative): each round solves a least-squares over ~2K-3K atoms, so the per-round cost grows with K +(~1.7 ms at M=12, ~57 ms at M=100 for N=400, D=1024). CoSaMP buys accuracy and few rounds with a per-round LS solve -- +a clear win at small-to-moderate K, while occlusion's cheap per-pick subtraction (with the SPEED-1 Gram) stays +attractive at very large K or when an approximate recovery suffices. + +THE RECOVERY FAMILY, COMPLETE (four members, each with its regime): + * linear -- one-shot correlation + top-m. Cheapest; washes out at load. + * occlusion -- greedy matching pursuit. One pass, can't revise; degrades on coherent dictionaries; cheap per pick + (and the SPEED-1 Gram makes it ~23x faster). Heaviest-recovered-first is its robust claim. + * IHT -- projected gradient descent (GRAD-1). Revises its support; holds at high coherence (0.71) better than + greedy; needs mu/K/iterations; reduces to lstsq at K=N (the GRAD-2 bridge). + * CoSaMP -- batch LS + prune (SPEED-3). STRONGEST: perfect across coherence, exact coefficients, ~2-3 rounds; + cost = a per-round LS solve; falls off at M/D > ~0.3 (the phase transition no method beats). + +Tests: +8 (1552 -> 1560). test_holographic_cosamp.py (+7): perfect across coherence; beats occlusion coherent; exact +coefficients; few rounds; the phase-transition cliff; descending by magnitude; deterministic. test_integration.py +(+1): CoSaMP through the mind (near-perfect coherent, ahead of IHT and occlusion, few rounds). Files: +holographic_cosamp.py, test_holographic_cosamp.py, holographic_unified.py (cosamp_recall faculty after iht_recall), +test_integration.py, README, NOTES_concepts.md, tour.py. + +*** SPEED-3 from the occlusion-speed panel review -- the M-factor, and the recovery family's strongest member. With +the four routes side by side and their regimes measured, only the N-factor remains: SPEED-2 (HoloForest-accelerated +atom selection -- Approximate MP / MIPS, approximate so its F1 cost must be measured, shipped last). *** + + +-------------------------------------------------------------------------------- +SPEED-2 -- FOREST-ROUTED OCCLUSION SELECTION (the N-factor; SHIPPED AS A KEPT NEGATIVE). The occlusion-speed analysis +named three factors in occlusion's O(M*N*D) cost. SPEED-1 removed D (cached Gram). SPEED-3 removed M (CoSaMP batch +rounds). SPEED-2 is the N factor: occlusion's pick-the-most-relevant-atom step is a max-inner-product search over the +whole dictionary, and a HoloForest answers it by comparing only the atoms ROUTED to the query's leaves -- genuinely +sub-linear in N. This is the last occlusion-speed item, and the panel flagged it "approximate, measure the F1 cost, +ship last." The measurement is the deliverable, and it is a kept negative. + +WHAT SHIPPED (holographic_occlusion.py extended; +2 mind faculties): + * build_occlusion_forest(codebook, n_trees=4, leaf_size=64, seed=0) -- a HoloForest over the codebook, built once + and reused across cues like the SPEED-1 Gram. + * occlusion_recall_forest(cue, codebook, m, forest=None, beam=4, ...) -- occlusion recall with the per-step + selection routed through the forest (recall_k for the most-similar unselected atom). Returns (index, weight) + descending, like occlusion_recall. + +MEASURED -- forest vs exact occlusion (D=512, M=12, 5 seeds), pushing N: + * N=500: forest F1 1.000, but SPEED 0.09x (11x SLOWER) -- the forest still compares ~94% of atoms; routing + overhead dominates. + * N=2000: forest F1 1.000, SPEED 0.20x, compares ~42%. + * N=5000: comparisons finally drop to ~12% (the sub-linearity is REAL) -- but F1 falls to 0.767 and it is still + 0.64x (slower). + +THE TWO KEPT NEGATIVES (loud -- this is why it is shipped as a documented regression, not a default): + 1. SPEED: the exact selection is a single vectorized BLAS matrix-vector product (codebook @ residual); the forest + routes through trees in Python per step. The routing overhead outweighs the saved comparisons until N is very + large -- a REGRESSION at every scale measured. Exact occlusion with the SPEED-1 Gram is the right default. + 2. ACCURACY: the forest is APPROXIMATE, so exactly when N is large enough to compare few candidates (~12% at + N=5000) it misses the true best atom often enough to drop F1 to ~0.77 (exact 1.0). The approximation cost + arrives precisely when the comparison saving does. + So this path is for the VERY-LARGE-N, approximate-acceptable regime only. (Same shape of lesson as the C-kernel + backend: a real mechanism that measures as a regression at operating dimensions -- kept callable so the failure is + visible, not hidden.) + +THE THREE-FACTOR PICTURE, COMPLETE: occlusion's O(M*N*D) -> + * D-factor: SPEED-1 cached Gram -- EXACT, ~23x, the default win. + * M-factor: SPEED-3 CoSaMP batch rounds -- EXACT, strongest recovery, ~2-3 rounds. + * N-factor: SPEED-2 forest selection -- APPROXIMATE, sub-linear comparisons but a regression at current scale (this + item). The exact paths win until N is enormous. + +Tests: +5 (1560 -> 1565). test_holographic_occlusion.py (+4): sub-linear comparisons; accurate at moderate N; never +beats exact (the approximation kept negative); deterministic. test_integration.py (+1): forest occlusion through the +mind (accurate at moderate N, sub-linear, exact matches or beats it). Files: holographic_occlusion.py (extended), +test_holographic_occlusion.py, holographic_unified.py (build_occlusion_forest + occlusion_recall_forest faculties), +test_integration.py, README, NOTES_concepts.md, tour.py. + +*** SPEED-2 from the occlusion-speed panel review -- the N-factor, measured to its honest conclusion. The +occlusion-speed backlog is now CLOSED: D-factor (Gram) and M-factor (CoSaMP) are exact wins; the N-factor (forest) is +a measured regression at operating scale, kept callable with its negatives loud. *** + + +-------------------------------------------------------------------------------- +W1 -- MIXTURE OF EXPERTS WITH A LEARNED GATE, WIRED (the wiring backlog begins). holographic_moe.GatedMixture -- a +bank of specialists plus a trained holographic gate (itself a creature brain) that routes each input to ONE expert, +learned from reward -- existed, tested, but was NOT a mind faculty (the only references in UnifiedMind were docstrings: +the module overview and a note in the soft-bone-mixture faculty calling itself the soft cousin of moe.GatedMixture). +This is the genuinely distinct routing the mind's own dispatch is NOT: decide/classify/recognize route by RULE (which +verb, what type), whereas the MoE gate is TRAINED, so it routes by the input's CONTENT -- which a type check cannot do. + +WHAT SHIPPED (+1 mind faculty mixture_of_experts; module + test wrapper already existed): + * mixture_of_experts(dim=None, seed=0, number_range=(-4,4)) -- returns a GatedMixture on the mind's dim/seed. Build: + add_expert(name, examples) / add_linear_expert(...) for specialists, train_gate(examples, epochs) to learn the + routing from outcomes, predict(x, modality) to infer. (Note: the training method is train_gate, not the "fit" the + backlog guessed -- the probe corrected it.) + +MEASURED (the module's own tests, now reachable through the mind): the learned gate routes two number-line experts by +VALUE at >=0.85 accuracy, beating either single expert by >0.25 (a type check could never do this); cross-modal it +beats any single expert by a wide margin and approaches the oracle; and it beats CONFIDENCE routing when a specialist +is confidently wrong (the outcome-trained gate is not fooled). Serves the Olshausen/Togelius seats -- learned, +interpretable routing. + +Tests: +1 (1565 -> 1566). test_integration.py (+1): the learned content-routing gate through the mind (two number-line +experts, routes by value, beats either single expert). The module's test_holographic_moe.py was already in the suite. +Files: holographic_unified.py (mixture_of_experts faculty, by reservoir/prototype_classifier), test_integration.py, +README, NOTES_concepts.md, tour.py. + +*** W1 from the wiring backlog -- highest value, a genuinely distinct routing capability now reachable through the one +mind. Remaining wiring: W2 kinematics (ties VSA-is-geometry), W4 versioned_store, W3 video codec, W5 graph_memory +(probe-first), C1 retire recurrent. *** + + +-------------------------------------------------------------------------------- +W2 -- CLOSED-FORM KINEMATICS, WIRED (the VSA-is-geometry thesis, pointed at motion). holographic_physics.Kinematics -- +physics as an algebra of binds: position += velocity is ONE binding, acceleration advances velocity the same way, and +the velocity BETWEEN two observed positions is read by UNBIND -- existed, tested, but was NOT a mind faculty (the only +'kinematics' hits in UnifiedMind were the FABRIK and blendpose INVERSE-kinematics docstrings, a different thing). This +is the direct embodiment of the engine's core thesis ('binding is a rigid shift') applied to motion, and the +CLOSED-FORM twin of the already-wired learn_dynamics (Propagator), which LEARNS its operator from data -- here the +operator is the encoder's own shift, exact by construction. + +WHAT SHIPPED (+1 mind faculty kinematics; module + test wrapper already existed): + * kinematics(dim=None, lo=-50, hi=50, seed=1) -- returns a Kinematics over [lo, hi] on the mind's dim. state(x), + step(S_x, S_v) (x += v as one bind), trajectory(x0, v0, a, steps) (integrate by pure binding, decode each + position; RAISES if the true path leaves the encoder's range -- the honest boundary), read_velocity(x_a, x_b) + (unbind two positions, decode). + +MEASURED (through the mind): a trajectory x0=0, v0=2 integrated by pure BINDING decodes to the true positions (max +error < 1.0 over 10 steps, endpoint x=20); the velocity between positions 10 and 13 is read by UNBIND as ~3.0; and a +path that leaves the encoder range (v0=100, 5 steps -> 500 >> hi=50) raises ValueError -- the boundary kept honest. +Serves the Stam/Macklin seats. + +Tests: +1 (1566 -> 1567). test_integration.py (+1): binding-is-motion through the mind (trajectory tracks truth, +velocity by unbind, out-of-range raises). The module's test_holographic_physics.py was already in the suite. Files: +holographic_unified.py (kinematics faculty, by learn_dynamics), test_integration.py, README, NOTES_concepts.md, +tour.py. + +*** W2 from the wiring backlog -- the closed-form kinematic twin of learn_dynamics, the cleanest demonstration that +binding is a rigid transform. Remaining wiring: W4 versioned_store, W3 video codec, W5 graph_memory (probe-first), +C1 retire recurrent. *** + + +-------------------------------------------------------------------------------- +W4 -- VERSIONED STORE WITH ROLLBACK, WIRED (undo/redo for the authoring vision). holographic_history.VersionedStore -- +a store whose every version is committed and exactly recoverable, history as keyframes + lossless row-keyed deltas +(the same keyframe/GOP structure the video codec uses, here for an edit timeline) -- existed, tested, but was NOT a +mind faculty (the only 'versioned' hit in UnifiedMind was a docstring about the kernel's versioned LOADER, unrelated). +This is the practical undo/redo and scene-versioning piece for the editable-mesh authoring vision the FS plan heads +toward, and a natural companion to the shipped scene_delta. + +WHAT SHIPPED (+1 mind faculty versioned_store; module + test wrapper already existed): + * versioned_store(gop_len=8) -- returns a VersionedStore on the mind's dim. new_id() for stable row ids, + commit(rows, order, proof=None, note='') (rows={id:vector}, order=[ids]; an optional proof(rows,order) gate must + return True or the commit is rejected and only logged -- proof-gated reorganization; returns the version index or + -1), checkout(version) (reconstruct any past state EXACTLY), rollback(version) (revert, itself recorded so history + is never erased), head(), history() (the audit of every attempt). + +MEASURED (through the mind): commit -> edit (add+change rows) -> checkout(v0) reconstructs the ORIGINAL state exactly; +rollback(v0) reverts the live state AND is a new recorded version (head grows, nothing erased); a proof that always +fails rejects the commit (returns -1), leaves the store unchanged, and the rejection is in the audit history. Serves +Duda (compression) and the editable-mesh authoring vision. + +Tests: +1 (1567 -> 1568). test_integration.py (+1): commit/edit/rollback through the mind (exact round-trip, proof +gate rejects, history never erased). The module's test_holographic_history.py was already in the suite. Files: +holographic_unified.py (versioned_store faculty, by scene_delta), test_integration.py, README, NOTES_concepts.md, +tour.py. + +*** W4 from the wiring backlog -- versioning/rollback now reachable through the mind, the undo/redo spine for the +editable-mesh vision. Remaining wiring: W3 video codec, W5 graph_memory (probe-first), C1 retire recurrent. *** + + +-------------------------------------------------------------------------------- +W3 -- MOTION-COMPENSATED VIDEO CODEC, WIRED (the rigid-shift-is-a-bind property as a codec). holographic_video.Holo- +graphicVideo -- a keyframe + motion-compensated-residual GOP coder: every gop_len-th frame is stored whole, the rest +as a one-number motion vector plus a holographically-compressed residual against the motion-shifted previous +reconstruction -- existed, tested, but was NOT a mind faculty (the only 'video' hit in UnifiedMind was a docstring +about the rigid-shift transform it uses). The mind had token/sequence compression and the rate-distortion code but no +image-domain motion-compensated codec; this is that, and the image twin of compress_lossless (both spend bits only on +what a predictor cannot foresee). + +WHAT SHIPPED (+1 mind faculty video_codec; module + test wrapper already existed): + * video_codec(dim=None, key_keep=400, res_keep=80, bits=8, gop_len=6, max_shift=8, seed=0) -- returns a Holographic- + Video on the mind's dim. encode(frames) -> (packets, total_bytes), decode(packets) -> frames, mean_psnr(frames, + packets), and the static intra_baseline(frames, keep, ...) to compare against. + +MEASURED (through the mind, on a rigid cyclic pan -- motion = exactly a roll): the GOP codec uses FEWER bytes (12922 vs +14112, ~8%) AND achieves HIGHER PSNR (49.8 vs 41.7) than per-frame intra storage -- the motion-compensation win, +because a shift is one bind that nearly zeroes the residual; decode round-trips to the right frames. KEPT NEGATIVE +(the module's own boundary): when the inter-frame change is NOT a rigid shift, the residual is large and the codec is +an honest LOSS -- rigid motion is the regime it wins. Serves Stam/Puckette (temporal) and Duda (compression). + +Tests: +1 (1568 -> 1569). test_integration.py (+1): GOP beats intra on a rigid pan through the mind (fewer bytes, +higher PSNR, decode round-trips). The module's test_holographic_video.py was already in the suite. Files: +holographic_unified.py (video_codec faculty, by compress_lossless), test_integration.py, README, NOTES_concepts.md, +tour.py. + +*** W3 from the wiring backlog -- the motion-compensated codec now reachable through the mind, rigid-shift-is-a-bind +made into compression. Remaining wiring: W5 graph_memory (probe-first), C1 retire recurrent. *** + + +-------------------------------------------------------------------------------- +W5 + C1 -- PROBED, AND HONESTLY NOT CHANGED (the wiring backlog closes with two measured non-actions). Both items the +backlog flagged as "probe first / cleanup" turned out, on measurement, to be correctly left as they are -- the engine's +discipline working as designed. + +W5 -- HIERARCHICAL GRAPH MEMORY: PROBED -> NOT WIRED. holographic_graph_memory.GraphMemory (a cosine-kmeans hierarchy, +the explicit hierarchical upgrade of the flat SelfOrganizingMind) was to be wired ONLY if it measurably beat the flat +memory at scale. Measured head-to-head vs a flat prototype store (256-dim Gaussian clusters, 12->400 labels): + labels | flat acc flat cmp | graph acc graph cmp + 12 | 1.000 12 | 1.000 10.4 + 50 | 1.000 50 | 0.920 20.1 + 150 | 1.000 150 | 0.743 26.4 + 400 | 1.000 400 | 0.547 33.5 +The hierarchy's COMPARISONS are genuinely sub-linear (10->33 while flat grows 12->400), BUT its ACCURACY COLLAPSES +(1.00->0.55) while the flat scan stays PERFECT (1.00) -- one wrong turn high in the tree is unrecoverable (high-dim +nearest-neighbour does not tree-route well). The flat scan is already optimal for classification; the sub-linear +comparisons do not compensate for the accuracy loss. VERDICT: NOT wired for classification -- consistent with the +module's OWN test docstring, which already records "NOT adopted for classification ... its home is sparse, navigable +structure (sequence/transition graphs), the same place the slime maze solver wins." Left as a documented alternative. +(The probe also re-confirmed: classify_vector returns (label, score), not the bare label.) + +C1 -- holographic_recurrent: NOT RETIRED, cross-referenced instead. The backlog flagged it as a superseded duplicate +of holographic_reservoir (which backs the mind's reservoir faculty). On probe it is NOT a mere duplicate: it carries +TWO reservoir flavours (classic-dense EchoStateNetwork + native VSAReservoir), a ReservoirSequenceClassifier USED BY +tour.py, and extensive honest real-corpora measurements (its own kept-negatives: a reservoir is a worse language model +than a gated net, measured on Alice/UDHR/Brown). Retiring it would break the tour and discard a measurement bench. +ACTION: added a one-line cross-reference note to its docstring -- the mind exposes the one clean reservoir via +holographic_reservoir.HolographicESN (the permute-based, substrate-native one); holographic_recurrent is kept as the +comparison/measurement home. No code behavior changed; nothing deleted. + +THE WIRING BACKLOG IS NOW CLOSED: W1 (mixture_of_experts) wired, W2 (kinematics) wired, W4 (versioned_store) wired, +W3 (video_codec) wired, W5 (graph_memory) probed -> not wired (measured non-win), C1 (recurrent) documented -> not +retired (not a duplicate). Four genuine siloed capabilities brought into the one mind; two probe-first items honestly +left alone with the measurement on record. The "drawer of disconnected experiments" is now either wired or documented +as deliberately-standalone. Next backlog: the Field-First Sculpting plan (FS-1 brushes, FS-3 splat export, FS-2 sparse +band, FS-4 the loop, FS-5 FPE field) -- new build toward the 3D authoring app. + + +================================================================================ +FIELD-FIRST SCULPTING BACKLOG (the 3D authoring app -- new build, not wiring) +================================================================================ + +-------------------------------------------------------------------------------- +FS-1 -- IMPLICIT-FIELD SCULPT BRUSHES (the brush API the rest builds on). A surface is carried as a FIELD whose +level-set is the surface (marching_tetrahedra meshes it). FS-1 packages the field atoms as SCULPT BRUSHES: a brush is +a LOCAL, falloff-weighted edit of a field FUNCTION in a ball around a point. Sculpt the field, RE-EXTRACT correct +topology at any resolution -- the resolution-independent move (DynaMesh/Sculptris) a fixed-mesh pipeline cannot do. + +WHAT SHIPPED (holographic_sculpt.py; +1 mind faculty sculpt): + * falloff(d, r, kind) -- the radial weight (vectorized), MATCHING the shipped soft-selection brush + (geodesic_soft_selection): 'smooth' = smoothstep 1-(3t^2-2t^3), 'linear' = 1-t, exactly 0 beyond the radius. + * brush_inflate / brush_carve -- raise / lower the field in the ball (grow / shrink a high-inside surface). + * brush_smooth -- blend toward the local average (Laplacian smoothing of the field). + * brush_grab -- drag the field's domain by a vector inside the ball (pull the surface along). + * brush_flatten -- pull the field toward a target level. brush_pinch -- drag the domain toward the brush centre. + * apply_brush(fn, kind, p, r, s, **kw) -- dispatch by name (grab needs drag=, flatten needs level=). + * UnifiedMind.sculpt(field_fn, kind, p, radius, strength, **kw) -- returns the edited field function. + +THE KEY GUARANTEE (FS plan's "Done when", measured): a brush leaves the field BIT-IDENTICAL outside the ball (all six +brushes change the field by <1e-12 at every point past the radius -- the falloff is exactly 0 there), so the surface +changes ONLY where you brushed; inflate GROWS the surface (304->624 cells above the mesh level) and carve SHRINKS it +(->32) -- the expected signed move in the band; the re-extracted mesh stays MANIFOLD (3120 faces). Works on ANY field: +the same inflate brush raises a value landscape locally (REWARD SHAPING -- the radius+falloff bare `reinforce` lacks), +unchanged outside the ball. Deterministic (pure functions, no RNG). + +KEPT HONEST: on a DENSE field the re-extract is still O(res^3) per stroke -- FS-2 (the narrow band) is what makes a +stroke cost O(brush); FS-1 is the brush math, not yet the fast representation. A grab/pinch dragging past the band can +fold the level set; keep the drag within the radius (FS-2's reinitialize re-distances). The brushes are Euclidean +(3-D geometry); the hypersphere Field generalizes with an angular metric (same construction). + +Tests: +9 (1569 -> 1578). test_holographic_sculpt.py (+8): inflate grows/carve shrinks; all six brushes local; +re-extract manifold; grab displaces inside only; reshapes a value field; falloff shapes + zero beyond radius; +dispatch + unknown; deterministic. test_integration.py (+1): sculpt brush local re-mesh through the mind (local, +grew, manifold). Files: holographic_sculpt.py, test_holographic_sculpt.py, holographic_unified.py (sculpt faculty, +by mesh_to_sdf), test_integration.py, README, NOTES_concepts.md, tour.py. + +*** FS-1 from the Field-First Sculpting plan -- the sculpt-brush API, on any field, with the local-edit guarantee +that makes resolution-independent re-meshing work. Next: FS-3 splat export (the .ply/JSON adapter, share +principal_axes with steering/QEM), then FS-2 the narrow-band sparse field (the hard one), FS-4 the loop, FS-5 FPE. *** + + +-------------------------------------------------------------------------------- +FS-3 -- SPLAT EXPORT: THE .ply / JSON ADAPTER (display a field as splats). The splat parameters are already in hand +(aniso_fit returns (center, amp, L), L the Cholesky of the inverse covariance), so this is a FORMAT ADAPTER: write the +engine's Gaussians to what a browser splat renderer reads, so a field/scene can be DISPLAYED as splats (the GPU's job; +the engine stays the authoring brain). + +WHAT SHIPPED (holographic_splatexport.py; +2 mind faculties export_splats, field_to_splats): + * principal_axes(precision) -- THE core math: eigen-decompose the symmetric precision P = L Lᵀ into (scales, + rotation), scale_i = 1/sqrt(eigenvalue_i) (the per-axis std), rotation = the eigenvectors (a proper rotation). + The L -> scale+rotation conversion. Raises on a non-PD (degenerate/flat) covariance -- surfaced, not faked. + * rotation_to_quaternion / quaternion_to_rotation -- the 3DGS rot_0..3 quaternion. + * splats_to_json / splats_from_json -- compact JSON for a three.js Gaussian-billboard shader. + * splats_to_ply / splats_from_ply -- the STANDARD 3D-Gaussian-Splatting .ply (INRIA binary layout: log-scale, + logit-opacity, SH-DC colour) that opens in any 3DGS viewer, plus a reader to round-trip-test it. + * field_to_splats(centers, radius) -- pull a metaball field's Gaussians directly (no fit; centres ARE positions, + radius IS the isotropic std -> L = (1/radius) I). + * UnifiedMind.export_splats(splats, path, fmt='ply'|'json', colors) and field_to_splats. + +MEASURED (round-trip is the "Done when"): principal_axes reconstructs a known SPD covariance to 1e-9; the quaternion +round-trips (matrix->quat->matrix); the .ply export->re-import recovers the covariance (1e-5), position, base colour, +and opacity; JSON round-trips; field_to_splats turns a metaball radius into the splat std exactly; a flat/degenerate +covariance is RAISED, not garbage. Deterministic. + +PROBE-CORRECTION (kept honest): the build plan expected the L->principal-axes math to live in THREE places already +(splat export, QEM, the steering kernel) and asked for one shared principal_axes helper. The LIVE-code probe DISPROVED +that: the steering kernel uses DIAGONAL bandwidths (its own kept negative -- a full covariance overfits); QEM SOLVES a +3x3 linear system (argmin vᵀQv, midpoint fallback when singular); consolidation's KLT is an SVD of a DATA matrix. +None eigen-decomposes a 3x3 form into principal axes. So principal_axes is built cleanly HERE, where the conversion +genuinely lives, and NOT retrofitted into modules doing different math (that would make them worse, not shared). The +"three call sites" was an over-optimistic plan assumption; the finding is recorded rather than forced. (This is the +plan's own warning -- "re-probe; this codebase keeps turning out to already have things under another name" -- applied +in reverse: it turned out NOT to have them the way the plan assumed.) + +KEPT HONEST: base colour only -- no view-dependent spherical-harmonic colour (a further add, noted not faked); a +degenerate covariance raises (QEM's singular-guard discipline). + +Tests: +8 (1578 -> 1586). test_holographic_splatexport.py (+7): principal_axes reconstructs covariance; quaternion +round-trip; .ply round-trip (covariance/position/colour/opacity); JSON round-trip; field_to_splats isotropic std; +degenerate raises; deterministic. test_integration.py (+1): metaball field -> splats -> .ply -> re-import through the +mind. Files: holographic_splatexport.py, test_holographic_splatexport.py, holographic_unified.py (export_splats + +field_to_splats faculties, by splat_field), test_integration.py, README, NOTES_concepts.md, tour.py. + +*** FS-3 from the Field-First Sculpting plan -- a field can now be exported as splats for a browser renderer, with the +L->scale+rotation math built where it belongs and the plan's "three shared call sites" premise honestly corrected. +Next: FS-2 the narrow-band sparse field (the hard one -- O(brush) strokes), then FS-4 the loop, FS-5 FPE. *** + + +-------------------------------------------------------------------------------- +FS-2 -- THE NARROW-BAND SPARSE FIELD (the hard one: O(brush) strokes, local re-extraction). FS-1 sculpts a field but +re-meshes the WHOLE res^3 volume per stroke -- batchy. The level-set field literature's fix (Adalsteinsson & Sethian's +narrow band; Museth's VDB/OpenVDB) is to store, edit, and re-extract ONLY the thin shell of voxels around the surface +(|f| < band). A brush then touches O(brush) voxels, and only the dirtied bricks re-mesh -- the thing that makes +sculpting interactive. + +REPRESENTATION (holographic_sparsefield.py; +1 mind faculty sparse_field): + * Voxels grouped into BRICKS of `tile` cells/edge; a brick owns the (tile+1)^3 corner block and OVERLAPS its + neighbours by one voxel plane, so a shared seam has identical world coords AND identical values -> the seam + meshes weld watertight. + * A brick is ACTIVE iff the surface passes through it. Only active bricks exist (sparsity at the brick level). + * The field is held in ONE global dict {(i,j,k): clamped_sdf} over every voxel of every active brick -- true SDF in + the band, clamped +/-band (correct sign) outside. One value per global voxel keeps seams exactly consistent (the + reinit and extract both rely on this). Bricks are materialized into a small dense array on demand for marching. + * THE EXACTNESS FACT: marching only makes faces where the field crosses 0, which only happens in the band; filling + far voxels with +/-band (correct sign) makes the SPARSE extraction identical to a full dense extraction of the + SURFACE (the far values never cross 0, so never make a face). + +API: SparseField.from_field(field, bounds, voxel, band, tile) [one-time O(res^3) seed]; sample(points) [trilinear in +the band, +band far -- the honest narrow-band limit]; apply_local(delta_fn, p, r) [edit only voxels in the ball; +returns (dirty_bricks, touched_count); delta ADDED -- lower f to inflate an SDF, raise to carve]; +reinitialize(iters) [GODUNOV UPWIND reinit phi_t = sign(phi0)(1-|grad phi|) with a smeared sign -- THE GENUINELY NEW +NUMERICS; central differences are unstable for this PDE]; extract_local(dirty_bricks) [marching_tetrahedra on the +dirty/active bricks only, each in its own world axes, POSITION-WELDED into one watertight mesh]. + +MEASURED (sphere SDF ground truth, res 48, tile 6): band SPARSE = 31610 stored / 117649 full voxels (27%); sample +matches the true SDF in-band; sparse extract matches a full dense extract within a voxel (Hausdorff 0.0269 < 0.0417) +and is WATERTIGHT/manifold across brick seams; a brush touched 888 voxels = 0.8% of the grid (the O(brush) win, vs +re-meshing all of res^3); reinit moved |grad f| from 0.469 toward 1 (0.767 after 12 Godunov iters -- a clear +improvement; a thin band + clamped edges limits how close, more band/iters -> closer). Deterministic (sorted-key dict +iteration, no RNG). + +REUSE (as-above-so-below): brick addressing IS _tile_bucket (the floor-divide tiling StructuredIndex/TiledStore/the +splat tiler share); the per-brick mesh IS marching_tetrahedra (the meshbridge weld); the bounded dirty-set is the same +idea occlusion_recall and select_lod embody (work a small active set, not the whole). + +KEPT HONEST (the negatives the plan demanded up front): pure-Python per-brick marching is real -- past a few hundred +active bricks the Python loop dominates and this belongs on the GPU (a compute shader over the dirty bricks). holostuff +is the AUTHORING BRAIN (which voxels are dirty, band bookkeeping, the SDF reinit numerics); the per-frame voxel grind +is the GPU's muscle -- the boundary the whole 3D plan draws. The band MUST be reinitialized or distances drift (shown +via |grad| before/after). TOPOLOGY growth into far/unseeded INTERIOR space is seeded as outside (+band) -- correct for +inflating into empty space (the common case), WRONG for growth into interior space or a merge/split, which need a +from_field re-seed. Documented, not silently mis-handled. + +Tests: +7 (1586 -> 1593). test_holographic_sparsefield.py (+6): sparsity; sample-in-band; extract watertight & on the +surface; stroke touches O(brush) not res^3; reinit moves |grad| toward 1; deterministic build. test_integration.py +(+1): build sparse field -> local inflate -> re-mesh dirty bricks through the mind. Files: holographic_sparsefield.py, +test_holographic_sparsefield.py, holographic_unified.py (sparse_field faculty, by sculpt), test_integration.py, +README, NOTES_concepts.md, tour.py. + +*** FS-2 from the Field-First Sculpting plan -- the hard one: a stroke now costs O(brush), not O(res^3), with the band +bookkeeping + Godunov reinit numerics done CPU-side and the per-frame voxel grind honestly handed to the GPU. Next: +FS-4 the loop (surface_mesh wrapping marching+select_lod; assemble sculpt->surface_mesh->splats), then FS-5 FPE +(research, probe only). *** + + +-------------------------------------------------------------------------------- +FS-4 -- THE SCULPT LOOP (surface_mesh; the loop named as an iterate-a-projection). FS-1 sculpts, FS-2 stores/edits the +band, FS-3 exports splats. FS-4 is the one drawable-mesh call that closes the cycle, composing parts already shipped. + +WHAT SHIPPED (+1 mind faculty surface_mesh; pure composition, no new module): + * UnifiedMind.surface_mesh(field, bounds, resolution, level, pixel_budget, distance, lod_targets, ...) -- turn ANY + field rep into the drawable mesh at the right detail: a field FUNCTION (via mesh_from_sdf) OR a SparseField (via + its local marching). With pixel_budget set, the mesh is run through mesh_lod_chain + mesh_select_lod and the + COARSEST level whose screen-space error at `distance` stays under the budget is returned (full detail near, + cheaper far). No new extraction -- it composes mesh_from_sdf / SparseField + the LOD faculties. + * NAMES THE LOOP as an iterate-a-projection (the resonator / denoiser / dynamics shape): each sculpt step EDITS the + field (apply_local) then RE-PROJECTS to the surface (re-extract). sculpt/sparse-edit -> surface_mesh -> + export_splats is the cycle; the field is the source of truth, mesh and splats are two re-projections of it. + +MEASURED: surface_mesh returns a watertight mesh from both a function field and a SparseField; with a far-distance +pixel budget the returned mesh is COARSER than full detail (the LOD selection composed through); the loop re-projects +the field to display splats too. KEPT HONEST: the LOD error is geometric surface deviation, not silhouette/perceptual; +the function-field path is dense O(res^3) (use the SparseField path for the O(brush) interactive loop); QEM LOD +decimation is pure-Python and slow (the lod module's own kept negative -- tests use a tiny mesh / one decimation). + +FS-2 CORRECTION (kept loud -- a real honest-measurement moment): while wiring FS-4 the sparse extract looked +non-manifold at one resolution (a sphere with voxel=2/30 -> a 31^3 grid). PROBED: it was NOT a sparse-field bug -- a +DENSE marching_tetrahedra at grid 31 is ALSO non-manifold (8928 faces), and the sparse extract reproduces it EXACTLY. +marching_tetrahedra is itself non-manifold at grid sizes where a vertex lands on the isosurface (pre-existing, +grid-dependent). So extract_local was refactored to materialize a CONTIGUOUS box and march it in ONE pass (a single +pass is always consistent; the prior per-brick weld could crack at a seam-parity flip) -- now BIT-EXACT to dense on +the same grid (verified: equal face counts, Hausdorff 0.0). A compact 1-byte/voxel SIGN grid (self.sign) is kept +alongside the band so any box materializes exactly (the VDB split: sparse band VALUES + coarse topology); COMPUTE +stays O(brush)/O(dirty). Lesson logged: "non-manifold" was a grid-size misattribution; the fix made the bound tighter +(exact) and the watertightness claim honest (it is the extractor's, grid-dependent, not the band's). + +Tests: +2 (1593 -> 1595, both integration; surface_mesh is composition -> integration tests, no unit module). +test_integration.py: surface_mesh extract paths (function field + SparseField, watertight); budget coarsens at +distance + loop to splats. Files: holographic_unified.py (surface_mesh faculty, by sparse_field), holographic_sparsefield.py +(extract_local single-march + sign grid + the grid-manifold note), test_integration.py, README, NOTES_concepts.md, tour.py. + +*** FS-4 from the Field-First Sculpting plan -- the loop closes: sculpt/sparse-edit -> surface_mesh (budget-aware) -> +export_splats, named into the iterate-a-projection family. Field-First Sculpting FS-1..FS-4 are done; FS-5 (FPE +function-field, surface-as-vector, edit-as-bind) remains -- research/probe-only, ship nothing on faith. *** + + +-------------------------------------------------------------------------------- +FS-4 PERFORMANCE PASS -- push the sculpt loop past the Python bottleneck (array-backed field + a working-set cache). +The thesis: do as much geometry as possible the PARALLEL way (vectorized NumPy / reused VSA machinery), because Python +per-element loops -- not arithmetic -- are the cost, and the round trip from a representation to Python triangles is +the expensive trip to stop paying for every frame. Probe-first found the solutions already in the codebase, the usual +lesson. + +TWO REFRAMES (both reuse existing patterns): + +1. ARRAY-BACKED FIELD (holographic_sparsefield.py rewritten). FS-2 stored the field as a Python DICT of voxels -- the + traditional, loop-heavy choice, and exactly what made the loop slow. Now the field is a DENSE NumPy ARRAY, so every + op is a vectorized array op with NO per-voxel Python loop: sample = one fancy-indexed trilinear gather over all + points; apply_local = a boolean-masked sub-array add over the brush ball; reinitialize = the Godunov update written + with np.roll across the whole band at once; _materialize = a pure slice. The public API is unchanged; `.values` is + kept as a compat dict property (rebuilt from the array). + +2. BRICK-MESH WORKING-SET CACHE -- the ReflexCache idea (holographic_tree), applied to geometry. ReflexCache thickens + the veins it travels often and skips the expensive path for a FAMILIAR input. extract_cached caches each brick's + extracted sub-mesh and, on re-extract, REUSES the bricks a stroke did not touch -- only DIRTY (or newly-active) + bricks are re-marched, welded by position (bit-exact at seams). Per-frame re-extract is O(dirty), not O(all + active). apply_local marks touched bricks dirty; cache_clear() if you mutate self.field directly (GramCache + discipline). Wired as UnifiedMind.surface_mesh(field, cache=True) for a SparseField. + +MEASURED (sphere, res 48, tile 6): extract still BIT-EXACT to dense (23472 faces) and watertight; the CACHE -- cold +marched 128 bricks, a brush touching 888 voxels dirtied 48 so the warm re-extract re-marched only 28 (~5x fewer +bricks, 1499ms -> 609ms), SAME surface; reinit (vectorized Godunov) moved |grad| 0.71 -> 0.77 toward 1 while +PRESERVING the 0-level set (so the surface -- and the mesh cache -- is unchanged by a reinit, a useful property). The +cache win GROWS with scene size (more cold bricks, the same small warm count for a local brush). + +WHY THESE AND NOT A FULL VSA-NATIVE MARCHING: the field SAMPLING and EDIT are now parallel array ops; the +working-set CACHE removes redundant marching from the per-frame path -- both grounded, low-risk, and reusing shipped +patterns (the tiling is still _tile_bucket, the mesh is still marching_tetrahedra, the cache is ReflexCache's +philosophy). Carrying the surface itself as a single hypervector (edit = bind) is the FPE research item (FS-5), kept +separate and probe-only. + +KEPT HONEST: the dense field is O(res^3) MEMORY -- a deliberate speed-for-memory trade (Python time is the cost, not +bytes; res 48 is < 0.5 MB); block-sparse allocation (only active bricks) reclaims memory-sparsity at large res, a +documented next step. The marching itself is still pure-Python per cell -- the cache removes it from the per-FRAME +path, but the COLD first extract still marches every active brick (a vectorized marching is the next step if cold +start must be fast too). np.roll wraps at the grid edge, but the band is interior so the wrap never touches it. + +Tests: +2 (1595 -> 1597). test_holographic_sparsefield.py (+1): the cache skips unchanged bricks (cold vs warm +marched count, same surface). test_integration.py (+1): cached sculpt loop re-marks only dirty bricks through the +mind. Files: holographic_sparsefield.py (array-backed rewrite + extract_cached/cache_clear), holographic_unified.py +(surface_mesh cache= param), test_holographic_sparsefield.py (reinit mutates the dense field now; cache test), +test_integration.py, README, NOTES_concepts.md, tour.py. + +*** FS-4 performance: the loop's field ops are now vectorized parallel NumPy and the per-frame re-extract is O(dirty) +via the ReflexCache-style brick cache -- the geometry fast path, built from patterns the codebase already had. *** + + +-------------------------------------------------------------------------------- +FS-4 PERFORMANCE PASS II -- the marching itself goes parallel (the case-table RAM). After pass I (array-backed field + +the ReflexCache-style brick cache), the bottleneck left was the MARCHING: a Python per-cell triple loop, and a cold +extract that round-trips the whole surface through Python. The reframe (the recurring "geometry as VSA-style parallel +ops" thesis): marching is a per-cell CASE LOOKUP -- each tetrahedron's triangles depend only on its 4 corners' +sign-pattern, indexed into a fixed table. That table IS a content-addressable lookup (the RAM/reflex pattern, +alongside ReflexCache/ReflexArc/HolographicMemory in the codebase), and it was the last thing trapped in a Python +loop. + +WHAT SHIPPED: holographic_meshbridge.marching_tetrahedra_vec -- a VECTORIZED marcher. A precomputed 16-entry +_TET_CASE_TABLE (sign-pattern -> triangle topology + the orient-toward point) is the RAM; the whole grid is processed +as parallel NumPy: gather every cell's tet-corner values by strided slicing, bit-pack the case index, gather triangles +from the table per case, dedupe crossings by a packed edge key (np.unique), interpolate all crossing positions at +once, and orient with one batched normal test. The only Python loop is 6 tets x 14 active cases -- a fixed count, +independent of grid size. marching_tetrahedra (per-cell) is kept as the reference. + +MEASURED -- geometrically IDENTICAL to the per-cell marcher (same vertex/face counts, same faces by position, same +orientation where defined, same manifoldness INCLUDING the grid-31 non-manifold case), Hausdorff ~1e-16 (machine eps, +a faithful parallelization not a different algorithm). SPEED: res 32 618ms->44ms (14x), res 48 1690ms->145ms (12x), +res 64 3577ms->400ms (9x), res 80 6280ms->1010ms (6x). In the sparse field selftest the whole surface marches in +163ms vs 1784ms per-cell Python (11x). Wired: the SparseField extracts (extract_local, extract_cached) now use the +vectorized marcher; UnifiedMind.mesh_from_sdf(..., vectorized=True) and surface_mesh's function-field path use it too +(default vectorized=False keeps the per-cell vertex ordering for exact backward compatibility) -- so the fast marcher +is available to any caller, elevating it as shared geometry infrastructure. + +HONEST FINDING (kept loud): the vectorized marcher is so fast that it MOSTLY OBVIATES the brick-mesh cache at current +scales -- a single vectorized march of the whole surface (~163ms) BEATS the cached per-brick path (~364ms warm), +because once marching is cheap the per-brick Python WELD dominates extract_cached. The cache still wins in the regime +where marching cost >> weld cost (very large surfaces); at small/medium scenes, extract_local with the vectorized +marcher is the fast full extract. So: vectorizing the hot loop beat caching around it -- the parallel reframe was the +bigger lever. Both are kept; the regimes are documented. Further work if needed: vectorize the cross-brick weld +(the next Python residue), and block-sparse field allocation for memory at large res. + +KEPT (unchanged): the vectorized marcher allocates O(cells) temporaries (a memory-for-speed trade, fine at these +sizes); like the per-cell marcher it is non-manifold at grid sizes where a vertex lands on the isosurface (it +reproduces that exactly); the dense field is O(res^3) memory. + +Tests: +1 (1597 -> 1598). test_holographic_meshbridge.py: marching_tetrahedra_vec matches the per-cell marcher +(sphere, two spheres, torus, and a non-manifold grid -- counts, faces-by-position, manifoldness, orientation). +Files: holographic_meshbridge.py (marching_tetrahedra_vec + _TET_CASE_TABLE), holographic_sparsefield.py (extracts +route through it; selftest reports the speedup), holographic_unified.py (mesh_from_sdf vectorized= param; surface_mesh +uses it), test_holographic_meshbridge.py, README, NOTES_concepts.md, tour.py. + +*** FS-4 performance II: the marching is now massive-parallel array ops driven by a sign-pattern RAM lookup -- 6-14x +faster, geometrically identical, available to every caller. The honest twist: parallelizing the marcher beat caching +around it. *** + + +-------------------------------------------------------------------------------- +GEOMETRY VECTORIZATION SWEEP -- move the recent geometry's Python loops to parallel NumPy (VSA-as-GPU). After the +vectorized marcher, swept the rest of the recent geometry stack (mesh kernel, LOD/QEM path) for per-element Python +loops and moved the genuinely-parallel ones to array ops, MEASURING each and keeping the one that can't move. + +SHIPPED (two clean wins, both verified identical to the loop): + * surface_deviation (holographic_meshqem) -- the LOD quality metric (mean/max point-to-surface distance from a's + vertices to b's faces), called on EVERY LOD level. Was an O(Va*Fb) scalar branchy Python double loop -- ~16000ms + on a 2160-face mesh. Vectorized: loop once over b's faces and compute each face's closest point to ALL of a's + vertices at once (_closest_point_on_triangle, already vectorized in meshbridge), running min -> O(Fb) array ops. + ~966ms now (~16x), results identical to 1e-16 (it feeds a reported metric, not a topology decision). + * Mesh.vertex_normals (holographic_mesh) -- Newell's method, run in the splat-export/render path. Was a per-face + + per-vertex-of-face double loop (~170ms @ 22k faces). Vectorized triangle fast-path: Newell cross-terms over all + faces at once, then a single FACE-ORDER np.add.at scatter (matches the loop's accumulation order EXACTLY -> + BIT-IDENTICAL); polygon meshes fall back to the loop. ~10x faster, bit-for-bit the same. + +MEASURED NEGATIVE (kept loud -- the bind_batch lesson again): + * vertex_quadrics (holographic_meshqem) CANNOT be vectorized in the obvious way. The plane offset uses a dot product + (n.dot(V[a])); its vectorized form (np.sum/einsum) sums in a different order and differs by ULPs. Those ULPs flip + QEM's collapse-order TIE-BREAKS and produce a DIFFERENT decimated mesh -- verified on three meshes (same face + count, different faces). A 1e-15 change in a tie-sensitive path is a real bug. So vertex_quadrics is kept in the + EXACT scalar form (it is called once per decimation and is not the bottleneck). A determinism test pins QEM's + output stable. The real QEM speedup (the 26s greedy collapse loop, which recomputes all edge costs every + iteration) needs a heap with incremental, ORDER-STABLE updates -- the documented 'delegate decimation to + meshoptimizer' negative stands; the naive vectorization is unsafe. + +THE PRINCIPLE CONFIRMED: vectorize the parallel, metric-like, decision-free loops (surface_deviation, vertex_normals) +-- big wins, identical results. Do NOT vectorize a tie-sensitive ordering input (vertex_quadrics) unless the +accumulation is bit-identical, which a dot product's summation order is not. "Use VSA/parallel where it's best at" +includes knowing where it ISN'T safe. + +Tests: +3 (1598 -> 1601). test_holographic_meshqem.py: surface_deviation vectorized == scalar reference (1e-12); QEM +decimation is deterministic (locks the vertex_quadrics-stays-scalar decision). test_holographic_mesh.py: +vertex_normals vectorized == loop (bit-identical) and points outward on a sphere. Files: holographic_meshqem.py +(surface_deviation vectorized; vertex_quadrics kept scalar with the negative documented), holographic_mesh.py +(vertex_normals triangle fast-path), test_holographic_meshqem.py, test_holographic_mesh.py, README, NOTES_concepts.md, +tour.py. + +*** Geometry vectorization sweep: surface_deviation 16x and vertex_normals 10x moved to parallel NumPy (identical +results); vertex_quadrics kept scalar because its vectorization flips QEM's deterministic tie-breaks -- the bind_batch +lesson, measured and respected. *** + + +-------------------------------------------------------------------------------- +FIELD-NATIVE LOD -- operate in the field, project the result (the mesh is a projection). The thesis the whole +geometry thread kept circling: the 3D mesh is a PROJECTION of the field, not the data itself. So the right way to get +a coarser mesh is to coarsen the SOURCE (the field) and re-project -- not to decimate the projection (QEM on the +mesh). Measured contrast that motivated this: QEM-decimating a 3480-face mesh to 1740 (greedy edge collapse) takes +~70,000 ms; re-marching the SAME field at a coarser grid to a comparable face count takes ~8 ms -- ~8000x, with no +edge collapse at all. + +WHAT SHIPPED: + * SparseField.extract_at_stride(stride) -- COARSEN THE SOURCE: subsample the dense field grid by `stride` and march + (one strided slice + one vectorized march). stride 1 = full field, 2 = half resolution per axis (1/8 the cells), + etc. The marched surface resolves to the coarse spacing, so coarser strides drop sub-cell detail -- LOD obtained + by re-projecting a coarser field. + * SparseField.lod_chain(strides=(1,2,4,8)) -- a field-native LOD chain. The elegant part: each level's error is + read AS A FIELD QUERY. A coarse marched vertex sits on the coarse 0-crossing; the FULL-resolution field value + there IS that vertex's signed distance to the true surface, so |sample(coarse_vertices)| is the level's deviation + -- O(V) field samples, NOT an O(V*F) mesh-to-mesh distance, and no greedy collapse. The error is also more HONEST + than QEM's: it's the distance to the TRUE surface (the field's 0-level), not the distance to the fine mesh. + * UnifiedMind.surface_mesh(field, pixel_budget=...) is now FIELD-NATIVE: it builds the LOD chain by re-marching the + source (SparseField.lod_chain, or _function_lod_chain for a field function) and picks the coarsest level under the + screen-space budget -- instead of QEM-decimating the fine mesh. The legacy QEM LOD (mesh_lod_chain) stays a + separate faculty for an IMPORTED mesh with no field behind it. + +MEASURED (sphere, SparseField at res ~40): the LOD chain (4 levels, 15840 -> 3960 -> 880 -> 144 faces, field-read +errors 0 -> 0.0049 -> 0.0203 -> 0.0928) builds in ~100 ms -- all re-marches, vectorized. surface_mesh budget=4px +returns 15840 faces at distance 0.5, 880 at distance 5, 144 at distance 50 (coarsens with distance), each in ~80-100 +ms. The old QEM budget path on the same 15840-face mesh would take MINUTES. + +WHY THIS IS THE RIGHT FRAME (and the honest boundary): the holographic spatial primitive already exists -- +VectorFunctionEncoder (FPE/VFA, holographic_fpe.py): a point is a hypervector, a whole field is a bundle, and a shift +of the entire field is ONE bind (exact). But a bundle has finite SNR (the capacity cliff: placed-vs-empty separation +0.39 at 2 atoms -> 0.02 at 32 -> 0.01 at 128), so you CANNOT cram a dense mesh into one hypervector and read vertices +back -- the field representation is for operating-on-wholes and querying, and you TILE under the cliff for many +distinguishable items (which the SparseField bricks already are). The lever is therefore not "make QEM one VSA op" +(its greedy order is a real data dependency, and the mesh won't fit a bundle) but to STOP decimating the projection: +keep the source field as the truth and re-project at the resolution the view needs. That re-projection IS one +operation, measured ~thousands x faster. + +KEPT HONEST: subsampling the field by a stride is a nearest-grid coarsening (a smoothing prefilter would be the +strict band-limit, but subsampling a clamped SDF preserves the bracketed 0-crossing while the surface stays larger +than the coarse cell -- exactly the LOD regime); the per-level error treats the finest available level as the +reference (error 0), with coarser levels' error read against the true field; `surface_mesh`'s `lod_targets` arg is +retained for signature compatibility but the field-native LOD coarsens by resolution stride, not face fraction. + +Tests: +1 (1601 -> 1602). test_integration.py: the field-native LOD chain coarsens monotonically with a +field-read error that only grows, builds fast, the budget coarsens with distance, and the chain's error EQUALS the +field-sampled deviation at the marched vertices. Files: holographic_sparsefield.py (extract_at_stride, lod_chain), +holographic_unified.py (surface_mesh field-native budget path, _function_lod_chain), test_integration.py, README, +NOTES_concepts.md, tour.py. + +*** Field-native LOD: coarsen the source field and re-project (~thousands x faster than QEM-ing the mesh), with the +per-level error read straight from the field. The mesh is a projection of the field -- now load-bearing in the +pipeline, exactly the path the panel converged on. *** + +================================================================================ +PARALLEL imported-mesh decimation + mesh->FIELD by tiling (the two paths an +imported mesh can take when there is no field behind it) +================================================================================ + +A field-backed surface uses the FIELD-NATIVE LOD (coarsen the source, re-march). +But a mesh that ARRIVES as a mesh has no field. Two new paths for that case: + +*** cluster_decimate (holographic_meshqem) -- the PARALLEL decimation. Vertex +clustering (Rossignac-Borrel / Lindstrom): bin vertices into a grid^3 lattice (the +engine's floor-divide tiling), collapse each cell to ONE representative, remap +faces, drop degenerate. Every step is a vectorized array op -- NO greedy +edge-collapse search -- so it is ~998x faster than greedy QEM (F2048->F1748 in 9ms +vs 8671ms). The representative is VSA-native: a cell's error quadric is the SUM +(a bundle, superposition) of its faces' plane tensors, and the representative is +that bundle's minimizer, clamped to the cell. "Sum of plane outer products = a +bundle" is the same algebra as the rest of the engine, here merging geometry. + + Vectorizing the quadric here is SAFE (unlike greedy QEM, where vertex_quadrics is + kept scalar): clustering has no collapse-order tie-break for a ULP to flip. Know + where parallel is safe -- the bind_batch lesson, read the right way. + + KEPT NEGATIVES: a coarse grid can go non-manifold (clustering trades quality and + manifoldness for parallel speed -- greedy qem_decimate stays the quality option). + The cluster LOD error is NOT monotonic in grid resolution: cell ALIGNMENT with the + surface matters, so a coarser grid can land representatives closer (grid10 error + 0.0019 < grid16 error 0.0108 on a sphere). The chain is monotone in FACE COUNT only. + +*** mesh_distance_grid (holographic_meshbridge) -- the mesh->FIELD direction by +TILING. Each triangle updates ONLY the local block of grid voxels within `band` of +it (a vectorized sub-array scatter-min by magnitude, the apply_local pattern), so +the cost is O(F*block) not O(F*res^3). Build once, then any number of query points +are O(V) trilinear samples (sample_distance_grid) -- the cheap point-to-surface +distance the brute O(Va*Fb) scan could not give. This is the gateway that lets an +imported mesh be queried like a field. + + THE KINK, and why it is SIGNED: an UNSIGNED distance field has a V-shaped kink at + the surface, so trilinear sampling there OVERESTIMATES by ~half a voxel and cannot + resolve sub-voxel distances (measured: 0.0069 vs a true 0.00045, ~15x off -- a kept + negative on the unsigned version). A SIGNED field crosses zero LINEARLY through the + surface, so |sample| near it is accurate to WELL under a voxel (0.00056 vs 0.00045). + Signing only the band voxels by nearest-face-normal is enough for sampling near the + surface -- no flood fill needed for that use. + + KEPT NEGATIVES: the build is currently an F-triangle Python loop (~1ms/triangle: + ~4.6s for 5352 faces, ~20s for 22k -- res barely changes it, confirming it is loop- + bound). The nearest-normal sign can mis-sign deep concavities / non-watertight meshes + (magnitude is always right). Far interior voxels default to +band (no flood-fill + sign), so this is a sample-NEAR-the-surface field, not yet a re-marchable full SDF. + +-------------------------------------------------------------------------------- +surface_deviation acceleration: TWO attempts, BOTH measured SLOWER, kept on record +-------------------------------------------------------------------------------- +The cluster LOD chain is gated by surface_deviation (the decimation is ~10ms/level; +measuring the error is the cost). Two tries to beat its O(Va*Fb) vectorized brute: + * Spatial-hashing b's triangles into a grid (the cluster binning) + a 3x3x3 + neighbourhood search per query vertex: ~0.5x (SLOWER); the chain went 6.3s->52s. + The pure-Python per-cell bookkeeping (cell->triangle lists, set unions) fragments + the work into many tiny vectorized ops, which LOSE to brute force's few LARGE + ones. Not even bit-exact (different min order). REVERTED. + * The signed banded grid above, sampled at the level's vertices: accurate (sub- + voxel) and ~2x on a 3-level chain (build once, sample many), but the grid build + is the same F Python loop -- a real but modest win, kept as the mesh->FIELD + capability rather than wired as the default error metric. + +THE REAL NEXT STEP (toolkit: move the geometric kernel to PARALLEL space): a BATCHED +closest-point-on-triangle (F triangles x B points, one vectorized op instead of an F +loop). It would make mesh_distance_grid, mesh_to_sdf, AND surface_deviation all fast +at once -- the single primitive under the whole point-to-mesh family. Then: add a +flood-fill SIGN to mesh_distance_grid -> a re-marchable full SDF -> an imported mesh +becomes a field -> it inherits the FIELD-NATIVE LOD (re-march coarser). That is the +decomposition/composition closure: operate in field space, project as 3D. + +================================================================================ +The batched closest-point kernel, and the MEASURED truth about cache: point-to-mesh +is MEMORY-BANDWIDTH-BOUND, and the per-triangle brute loop is already cache-optimal +================================================================================ + +GOAL was a batched closest-point-on-triangle to vectorize the per-triangle Python +loop under mesh_distance_grid / mesh_to_sdf / surface_deviation. The kernel was built +and is CORRECT: `_closest_points_on_triangles(P,A,B,C)` broadcasts Ericson's region +test over any leading shape -- PAIRED (F triangles x their own blocks) or ALL-PAIRS +(N points x F triangles) -- matching the single-triangle kernel to 2e-16. + +It did NOT make anything faster. Measured, and kept loud: + + * ALL-PAIRS (surface_deviation / mesh_to_sdf): the batched form is SLOWER than the + brute F-loop at EVERY chunk size -- and the cache-blocking sweep shows exactly why: + chunk 2: 15.7s (1.2MB working set) + chunk 8: 17.9s (4.9MB) + chunk 32: 19.3s (19.8MB) + chunk 128: 21.7s (79MB) + chunk 512: 32.9s (316MB) + brute F-loop: 11.5s + Smaller working set is faster (the memory-bound signature), but NO chunk beats + brute. Brute wins because it never materializes the (N,F) / (N,F,3) intermediates: + it streams ONE triangle at a time over a tiny (N,3) working set that stays + cache-resident, reusing the point array across triangles. That is the + cache-appropriate structure for this op. + + * PAIRED (mesh_distance_grid): batching the small local blocks LOSES too (0.7x at + chunk 128, worse larger) -- same memory-bound wall once (chunk, block) intermediates + grow. The per-triangle loop's small block stays in cache; the batch does not. + +THE CACHE LESSON (the real answer to "use the L2/L3 cache"): you cannot vectorize out +of a memory-bandwidth-bound reduction. The existing brute per-triangle loops ARE the +cache-aware structure -- minimal working set, point-array reuse, streamed triangles. +Batching into large vectorized intermediates DEFEATS cache residency and loses. So +surface_deviation and mesh_to_sdf are KEPT on the brute loop; the batched kernel ships +as correct infrastructure (and a convenience for moderate meshes), not as a hot path. + +THE CACHE MODULES do not rescue it either: + * holographic_cache.py (Ward gradient cache) interpolates a smooth field from sparse + value+gradient anchors -- but `interp_first_order` is PER-QUERY (O(anchors) per + point), so a dense gradient-cached SDF would be O(N_query x N_anchor) with a Python + loop over queries. No spatial structure -> same wall. + * holographic_adaptive_cache.py places anchors where a field bends -- about WHERE to + cache, not how to make a dense geometric reduction cache-fast. + * ReflexCache (holographic_tree.py) is a genuine fit for REPEATED queries against the + SAME mesh: memoize a built mesh_distance_grid keyed by (mesh, bounds, res) and reuse + it across calls (the cluster LOD chain already does this implicitly, build-once- + sample-many). That is a real but minor working-set win, not a kernel speedup. + +THE GENUINE SPEEDUP is ALGORITHMIC, not a vectorization or a cache trick: a VECTORIZED +spatial index (sort-based binning + searchsorted, NO Python per-cell dicts -- those were +also measured slower) that CULLS the O(N*F) work so each query only tests nearby +triangles. Then the per-candidate distances are few and the brute kernel is cheap. That +is the real next build. Everything tried so far -- Python spatial hash, full batching, +the batched kernel, the gradient cache -- has confirmed by elimination that culling the +work (vectorized) is the only lever left. + +================================================================================ +The decomposition CLOSURE: flood-fill the sign -> a full re-marchable SDF -> an +imported mesh becomes a field and inherits field-native LOD +================================================================================ + +mesh_distance_grid gives a BANDED signed SDF: the band carries exact signed distance, +but far-from-surface voxels default to +band, so the interior is wrongly positive -- +not re-marchable (marching it would find a spurious inner surface). flood_fill_sign +fixes that without touching a single triangle: + + * Flood the OUTSIDE inward from the grid boundary through {value >= 0}, a vectorized + iterative 6-neighbour dilation (array shifts). The NEGATIVE band shell around the + surface blocks the flood, so any far voxel the boundary cannot reach is ENCLOSED -> + interior -> set to -band. Converges in O(grid diameter) cheap passes. + +mesh_to_sdf_grid = mesh_distance_grid + flood_fill_sign = a FULL signed SDF with the +surface as a true zero level set. Measured on a sphere mesh: the interior centre flips +from +0.127 (banded, wrong) to -0.127 (filled); a far corner stays +; and re-marching +the full SDF reconstructs the surface (closed, vertices back on the sphere to 3 +decimals). + +THE PAYOFF (mesh_field_lod): an imported mesh -- one that arrived with NO field behind +it -- is now convertible to a field ONCE, then RE-MARCHED at coarser strides to get a +level-of-detail chain (40344 -> 10080 -> 2472 faces by striding). That is the SAME +field-native LOD that surface_mesh gives a native field (coarsen the source, re-project, +error read from the field), now reached by a mesh. The decomposition/composition loop is +closed in both directions: a field projects to a mesh (marching), and a mesh lifts back +to a field (tile the banded SDF, flood-fill the sign). Operate in field space, project +as 3D. + +WHY THIS, AND NOT MORE CACHE WORK: the flood fill touches no triangles, so it sidesteps +the memory-bandwidth wall that defeated the batched point-to-mesh kernel. It is the +right kind of move once the cache angle is exhausted -- a structural capability, not a +micro-optimization of a memory-bound reduction. The remaining slow step is still the +mesh_distance_grid BUILD (the per-triangle scatter F-loop), which is memory-bound to +batch; the genuine lever there remains a vectorized spatial index, unchanged. KEPT +HONEST: the flood fill needs a watertight negative band shell -- a too-thin band or a +non-watertight / nearest-normal-mis-signed mesh lets the flood leak (interior stays +positive); the band distances are always correct regardless. + +================================================================================ +The POSITIVE result: a vectorized spatial-grid index that CULLS the point-to-mesh +work -- 20-110x, exact near the surface (what batching could not give) +================================================================================ + +Every earlier attempt to speed point-to-mesh was a kept negative (Python spatial +hash, full batching, the batched closest-point kernel, the gradient cache) -- all +confirmed the same wall: the dense O(N*F) reduction is memory-bandwidth-bound and the +brute per-triangle loop is already cache-optimal. By ELIMINATION, the only lever left +was to do LESS WORK: a spatial index that culls the pairs. That is point_set_to_mesh_grid, +and it is a clean WIN -- and it is all array ops, NO Python per-cell dicts (those were +the original slow attempt): + + BUILD (vectorized): bin each triangle into its CENTROID cell; argsort triangles by + cell id; build a CSR [start,count] per cell with bincount + cumsum. + QUERY (vectorized): each query reads only the (2r+1)^3 cells around its own; the + ragged 'gather the triangles in those cells' is the vectorized RANGES trick -- build an + increment array whose cumsum IS the concatenated [start,start+count) index ranges -- + giving a flat (query, candidate-triangle) edge list; compute the exact closest-point + distance for those FEW edges (the batched kernel, paired) and reduce per query with + np.minimum.at. Signed picks the nearest triangle per query by lexsort(distance within + query) and signs by its face normal. + +This turns O(N*F) into O(N * candidates), candidates ~ (2r+1)^3 * (triangles per cell), +which is small for a surface mesh (most cells empty). MEASURED on a 22272-face mesh: + + * 11138 near-surface queries: brute 60.4s -> GRID(r=2) 3.0s = 20x, ZERO misses, max + error 1e-16 (machine epsilon -- EXACT for what it finds). r=1 is 0.24s, still 0 misses. + * 3-level cluster LOD chain error: brute surface_deviation 27.1s -> GRID 0.25s = 110x, + error IDENTICAL to brute (mean 0.00026 / max 0.00197), 0 misses. Now WIRED into + build_cluster_lod_chain, with a transparent fallback to the exact surface_deviation + for any level that has an out-of-reach vertex. + +KEPT HONEST -- APPROXIMATE BY CONSTRUCTION: a triangle sits in its centroid cell only, and +a query sees a finite radius, so the TRUE nearest is guaranteed only within `radius` cells. +Correct for near-surface queries on a roughly uniform mesh (decimation/LOD error, contact, +snapping -- the regime that matters); a large triangle whose centroid is far, or a far-field +query, can be missed, and a query whose neighbourhood is empty returns +inf (raise radius, or +use the exact brute path). This is the right tradeoff: exact where it is used, honest about +where it is not. The remaining mesh_distance_grid BUILD cost (per-triangle scatter) is +untouched -- a separate, still-memory-bound F-loop. + +THE ARC, end to end: batching loses (memory-bound) -> the cache modules are per-query +(no dense win) -> so CULL the work with a vectorized index -> 20-110x, exact near surface. +The negatives were not detours; they were the elimination that pointed straight here. + +================================================================================ +The work-culling lesson applied to the BUILD and to surface_deviation; and FS-5 +(the surface as a single hypervector, edit = bind) +================================================================================ + +Three pieces, two of them the spatial-index lesson reaching the rest of the stack, one the +last Field-First Sculpting backlog item. + +1. mesh_distance_grid BUILD -- now O(SURFACE AREA), not O(triangle count). The mesh->field build +used to SCATTER a signed distance from every triangle (a Python F-loop). The same "cull, don't +loop over everything" move that fixed the query fixes the build: mark the near-surface SHELL voxels +(voxels holding a vertex / centroid / edge-midpoint, dilated by the band width) and answer them with +ONE vectorized grid-culled point_set_to_mesh_grid query. method="shell" is the new default; +method="scatter" is kept as the bit-exact, large-triangle-robust fallback. MEASURED: 1.3x at 9.6k +faces, 2.7x at 22k, and the cost stops growing with the triangle count (it tracks surface area), so +the win widens on large meshes. VERIFIED EQUIVALENT for every use: near-surface samples match scatter +to 5e-17, flood-fill gives the right interior sign, the re-marched surface is closed. (Band-EDGE +voxels, clamped to +-band and never near the zero level, may differ in which get clamped -- benign.) +Shell query radius is 2 to bound memory (radius 3 OOMs the test suite at res 56+). + +2. surface_deviation(fast=True) -- the spatial index fulfils this function's OWN backlogged note. Its +docstring had said "the genuine fix is a fully VECTORIZED spatial index (one big gather, no Python +per-cell loop) -- a real build, backlogged." That build now exists (point_set_to_mesh_grid), so +fast=True routes through it: exact + ~84-110x faster for the case this metric is actually used in (a +decimated mesh vs its original, vertices near the surface), with a transparent brute fallback when +a's vertices fall outside the index's reach (two far-apart meshes). Every caller benefits -- the QEM +build_lod_chain for free, and build_cluster_lod_chain was consolidated to call surface_deviation +instead of its own inline grid. The greedy splat_merge (order-dependent used[] mask) and the QEM +vertex_quadrics loop (not a nearest-neighbour) are documented NON-fits, kept on record. + + THE WHOLE PERFORMANCE ARC, now complete: field->mesh (marching, parallel), mesh decimation + (cluster_decimate, ~1000x vs QEM), point-to-mesh QUERY (point_set_to_mesh_grid, 20-110x), the + mesh->field BUILD (shell, O(surface)), and the LOD deviation metric (surface_deviation fast) are all + either parallel or work-culled. The lesson held everywhere: batching the dense reduction is + memory-bound; culling the work with a vectorized index (no Python per-cell dicts) is the win. + +3. FS-5 -- THE SURFACE AS A SINGLE HYPERVECTOR (edit = bind). The last Field-First Sculpting item, and +the most literal "move the geometry into holographic space." holographic_fpefield.HolographicField +bundles a surface's SIGNED-distance samples into ONE FPE vector: f = sum_i sdf(p_i) encode(p_i) +(VectorFunctionEncoder). value(x) is one cosine query (negative inside, positive outside, crossing +zero at the surface); translate(delta) moves the WHOLE surface with a SINGLE bind; union merges two +surfaces by bundling; surface() re-extracts by marching the 0-level. (The RBF kernel is a Gaussian +bump in the hypervector domain -- this is Gaussian splatting carried in VSA space, the FS-3 splats' +cousin; the bandwidth is the same band-limit knob flagged for the fractal-optics work.) + + POSITIVE -- THE HEADLINE IS EXACT: translate gives value_shifted(x) == value_orig(x - delta) to + 1e-16, and the surface's +x zero-crossing moves by EXACTLY the delta, via one O(dim) binding. The + surface is genuinely one vector and moving/merging it is genuinely algebra. + + KEPT HONEST (all measured): (a) the FPE WRAPS at the encoder bounds, so bounds must exceed + |sample|+|shift| or a shifted sample aliases -- the early "shift did nothing" was exactly this plus a + noisy-mesh-centroid measurement; the value-field test is the clean one. (b) the marched 0-level is a + SMOOTHED, ~15%-biased (blur shrinks a convex SDF -- recovered sphere radius ~0.666 vs 0.6), + not-guaranteed-watertight estimate; bandwidth is the bias knob. (c) finite DIM is a roughness noise + floor (sphere-radius std ~0.13 at dim 1024 -> ~0.065 at dim 4096) -- the capacity trade of carrying a + continuous field in a fixed vector. (d) valid only WITHIN the sampled cloud (far outside, the kernel + sum decays into crosstalk, reading a spurious small negative). (e) FFT-bound build/extract -- a + DEMONSTRATION representation; the array-backed SparseField (FS-2/FS-4) stays the performance path. + The value is conceptual: the surface as one vector, edit = bind. Field-First Sculpting FS-1..FS-5 done. + +Faculties: mesh_to_field_vector (FS-5) wired into UnifiedMind. Tests +8 (1613 -> 1621): fpefield (5), +integration (1, FS-5 through the mind), meshbridge (1, shell==scatter+remarch), meshqem (1, +surface_deviation fast==brute+fallback). Files: holographic_meshbridge.py (shell build default), +holographic_meshqem.py (surface_deviation fast path), holographic_lod.py (build_cluster_lod_chain +consolidated), holographic_fpefield.py (NEW), holographic_unified.py (mesh_to_field_vector), +the four test files, README, NOTES, tour. + +================================================================================ +Delta editing of a model carried as ONE hypervector (the temporal/video-codec +insight, applied to geometry editing) -- O(edit), exact undo, model-size-independent +================================================================================ + +THE QUESTION (from the bench): the temporal/video work gave us history tracking and the +"a moving thing is one operator, store the delta" insight -- does that also let us EDIT a model +by just specifying the delta, applied in holographic space, touching only what changed? + +THE PROBE (done first, the project's rule). Three granularities of "delta" already exist or nearly do: + * scene_delta / apply_scene_delta (holographic_scenedelta) -- COMPONENT level: which whole mesh/transform + atoms a scene added/removed. Coarse (swap a mesh in a scene graph); content-addressing dedups the rest. + * SparseField.apply_local + extract_cached (holographic_sparsefield, FS-2/FS-4) -- ARRAY-VOXEL level: a brush + is an O(brush) masked sub-array update, and the ReflexCache-style brick cache re-marches ONLY the dirty bricks + (O(dirty), not O(all)). The "don't touch every vert/face" optimisation is ALREADY shipped for the array field. + * the video codec (holographic_video) -- the confirming insight: motion-compensation IS a bind, the residual IS + the delta. Moose's intuition was exactly right. + +THE GAP (the genuine build): bring that locality to FS-5, the model carried as ONE vector. The holographic field +has a property the array field does NOT -- it is a LINEAR superposition, so an edit is literally f + delta, and that +unlocks three things the array field can't match. Added to HolographicField (holographic_fpefield): + * make_delta(points, values) -> a FieldDelta (the edit as a hypervector, d = sum_i values_i encode(p_i)); building + it is O(edit) -- the brush, not the model. + * apply_delta(d) -> f + d.vec (one O(dim) add); remove_delta(d) -> f - d.vec. + * surface(sub_box) -> march only a region (local re-extraction after a local edit). + +MEASURED (the headline claims, all confirmed): + * UNDO IS EXACT: f + d - d == f to 4e-16 (machine precision). Because the field is a linear bundle, undo is exact + subtraction -- and a whole undo/redo HISTORY is just a list of compact delta vectors (subtract to undo, add to + redo). Two stacked edits compose and unwind exactly. + * THE EDIT IS MODEL-SIZE-INDEPENDENT: a 1728-sample model and a 17576-sample (10x) model are BOTH one dim-2048 + vector; applying the same brush delta costs the same on each (a microsecond vector add) -- the model's complexity + does not enter the edit cost at all. This is the answer to "handle large models edited in real time": in + holographic space the model is a fixed-length vector, so editing is O(edit), full stop. + * THE EDIT IS LOCAL: value at the edit point moved (-0.017 -> -0.061) while the far side held (-0.017 -> -0.018); + re-extracting only the dirty box marched 1728 vs 8000 points (~7x fewer) for a pole-sized edit, more for smaller. + +KEPT HONEST: this is still FS-5's smoothed/biased FFT-bound representation -- a model carried as a vector for compact +storage, transmittable deltas, exact-undo history, and algebraic transforms; NOT the fast marcher. The array-backed +SparseField (FS-2/FS-4) remains the path when you need fast faithful voxel editing + marching. And the delta is a +soft-kernel edit (a region re-extract box must include the kernel reach or it clips the edit's tail). The two +representations are complementary: the array field for fast local voxel sculpting, the hypervector field for a +compact model whose edits are deltas you can add, subtract (undo), bind (transform), and transmit. + +Tests +2 (1621 -> 1623): fpefield (1, delta add/exact-undo/history/model-size-independence), integration (1, delta +editing through the mind). Files: holographic_fpefield.py (FieldDelta + make_delta/apply_delta/remove_delta, surface +sub-box note), test_holographic_fpefield.py, test_integration.py, README, NOTES, tour. + +================================================================================ +Real-time sculpting at 30-60fps: extract_dirty -- project ONLY the changed +bricks, so the per-frame cost tracks the BRUSH, not the model +================================================================================ + +THE QUESTION (from the bench): a sculpting brush dragged over a mesh needs 30-60fps in the +viewport -- ingest the edit, apply it to the affected region, project that change back out, every +frame. Does the sparse-field sculpt loop hold that budget on a large model? + +THE PROBE (done first). The loop is SparseField.apply_local (O(brush) masked update) then +extract_cached. apply_local is fine. But extract_cached, though it re-marches ONLY dirty bricks, +then WELDS every active brick's faces into one mesh on every call -- a Python loop over every face +with a per-vertex dict lookup. That reassembly is O(TOTAL faces), and it is the bottleneck: + + MEASURED warm extract_cached after a brush touching a handful of bricks: + res 64, 128 bricks, 41k faces, 8 bricks re-marched -> 590 ms (~2 fps) + res 96, 224 bricks, 93k faces, 28 bricks re-marched -> 1365 ms (<1 fps) + + 18-85x over a 33 ms frame. The EDITING is local and fast; the per-frame full-mesh REASSEMBLY is + what blows the budget on a large model. (This is the same "never re-handle the whole heavy mesh" + principle from the 3D-app architecture, now as a sculpting-loop bug.) + +THE FIX: SparseField.extract_dirty() -- re-mesh only the bricks a stroke touched and return them as a +per-brick DELTA, never reassembling the whole surface: + + {'updated': {brick_id: Mesh}, 'removed': [brick_id, ...]} + +The viewport keeps a per-brick mesh map and swaps only the changed bricks (the renderer holds the +rest). Adjacent bricks share a voxel plane and march it identically, so the per-brick meshes meet at +BIT-EXACT seams (duplicated boundary verts, no cracks). A cold first call returns every brick (the +viewport's initial build); every later call returns just the stroke's delta. The cache state it +leaves is identical to extract_cached's, so the two interoperate. + +MEASURED (the answer): + * res 64, typical brush: warm extract_dirty 19.6 ms = 51 fps (was 590 ms) -- a ~30x cut, IN budget. + * res 96, fine detail brush (r=0.06-0.10, 4 dirty bricks): 14 ms = 70 fps (was 1365 ms). + * res 96, a big brush spanning 28 bricks: 78 ms = 13 fps -- it degrades with BRUSH size (more bricks + to march), not model size; a normal brush is well inside budget. + * MODEL-SIZE INDEPENDENCE: the same brush on a 156-brick model and a 216-brick model cost the same + few ms (the bigger model was actually faster -- it just tracks how many bricks the brush hit). The + per-frame projection is O(dirty), independent of total model size -- the whole point. + +So the sculpt loop is: apply_local (O(brush)) -> extract_dirty (O(dirty)) -> push the per-brick delta +to the renderer. A model of any size stays at 30-60fps under a brush, because nothing per-frame is +O(model) anymore. + +KEPT HONEST: the remaining per-frame cost is the dirty-brick MARCHING (vectorized, but still ~2-3 ms +per tile-8 brick), so a very large brush on a fine grid can exceed budget -- the lever there is a +smaller tile or batching the dirty bricks into one march (a documented next step, not done). The +per-brick deltas are unwelded across brick seams (fine for rendering; if a caller needs one watertight +indexed mesh it still calls extract_cached). This is the ARRAY-field (FS-2/4) fast path; the +hypervector field (FS-5) stays the compact/transmittable/exact-undo representation, not the marcher. + +Tests +1 (1623 -> 1624): sparsefield (extract_dirty cold=all / warm=dirty / per-brick correctness / +brush-bounded on two models). Also covered in the sparsefield selftest. Files: holographic_sparsefield.py +(extract_dirty), test_holographic_sparsefield.py, README, NOTES, tour. + +================================================================================ +CI robustness fix: a generation "setup is real" check was numpy-build-sensitive +================================================================================ + +test_holographic_misgen (the B1 MIS no-op probe) failed in CI but passed locally. The probe's REAL +assertion -- the no-op, abs(d_bal - d_verif) < 0.05 (the balance heuristic == verifier-only, a +STRUCTURAL fact) -- holds everywhere. What failed was the auxiliary "setup is real" check, which had +demanded the verifier be 1.3x more diverse than greedy. Dev numpy (2.4.4) gives ~3x; a CI numpy gave +~1.17x. ROOT CAUSE: _generate's per-step argmax is over a 512-dim structure score (a quadratic form), +and last-bit BLAS differences across numpy builds flip an early pick, which cascades the whole +generation into or out of the loop trap -- the same tie-sensitivity the bind_batch lesson is about, +now across numpy BUILDS rather than within a run. The engine's determinism rule is "deterministic +given a fixed environment (PYTHONHASHSEED=0)"; cross-BLAS bit-identity of a 512-dim quadratic-form +argmax is a guarantee numerical code does not make. FIX: assert the robust DIRECTION (d_verif > +d_greedy -- the verifier escapes the loop more than greedy, true in both environments) instead of a +brittle magnitude; the structural no-op stays strict. No behavior change, just a magnitude assertion +relaxed to the qualitative claim it was really standing in for. Files: holographic_misgen.py. + +================================================================================ +Holographic-native geometry & appearance: noise, materials, displacement, +terrain, grammar, attributes (G1-G6) [+40 tests: 1624 -> 1664] +================================================================================ + +Built the six-item geometry/appearance backlog as field-native faculties -- everything that sits on a +mesh living in the SAME holographic algebra as the geometry, so a textured, displaced object is one +composable hypervector. All additive, default-off, deterministic. + +G1 NOISE (holographic_noise.py). Band-limited procedural noise AS A FIELD: a single band is one +hypervector -- an FPE bundle of random-weighted RBF kernels, f = sum_i w_i encode(p_i) with w_i ~ N(0,1) +on a jittered lattice -- so querying it is a smooth Gaussian-process sample whose correlation length is +~1/bandwidth (measured lag-1 autocorr 0.998). fBm is the OCTAVE BUNDLE: a weighted superposition of +per-octave band fields at bandwidth base*lacunarity^o, amplitude gain^o; fBm(x) = sum_o gain^o query(band_o, x). +Roughness (normalized lag-1 variation -- wiggle per unit amplitude, the robust measure after a lag-1-on- +coarse-features attempt and a window-fragile high-pass attempt both mis-fired) tracks persistence +(0.031 -> 0.055 for gain 0.25 -> 0.90). KEPT NEGATIVE: smooth-spectrum only (no hard edges); FFT-bound +(each kernel is one encode), so kernels are capped per octave and deep fBm is expensive. + +G2 MATERIALS (holographic_material.py). The centerpiece and purest VSA fit. A texture is an FPE function +over UV (encoder.bundle(uv, values)); a material is a role-filler HRR record sum_r bind(role_r, channel_r) +with role atoms from hashlib(name) seeds (cross-run deterministic, NOT salted hash()). KEY LESSON from a +0.73-crosstalk failure: a small-norm channel (a height bump) gets SWAMPED by big-norm channels because +their scrambled crosstalk inflates the query denominator and rescales the small channel down (~0.3x). +TWO FIXES: (a) sample() reads the EXACT stored field (no crosstalk) -- the unbind path is only for +recovering from a BARE record after transmission; (b) the record binds UNIT-normalized channel directions +so capacity is BALANCED (recovery 0.48/0.48/0.46, no swamping; cosine is scale-free so values are +unchanged). transform_uv re-UVs every channel with ONE bind (bind associativity: bind(bind(role,f),shift) += bind(role,shift(f))); blend is linear. compose_object binds geometry+appearance under balanced roles; +recovery is a 2-item capacity cliff at dim 1024, so the test is a MARGIN check (each side recovers its own +>3x the other: app 0.545, geom 0.580) not an absolute cosine. KEPT NEGATIVE: band-limited (sharp masks +stay raster); bare-record recovery carries ~sqrt(n)/sqrt(dim) crosstalk. + +G3 DISPLACE/BUMP (holographic_displace.py). SDF displacement IS a field delta: make_delta with NEGATIVE +values pushes the surface outward (its documented sign), apply_delta adds it (O(edit)), remove_delta undoes +it EXACTLY (2.2e-16). Mesh displacement moves vertices along normals by amount*scalar; bump tilts shading +normals from the scalar's tangential slope without moving vertices. KEPT NEGATIVE: the SDF path is the +near-surface shader approximation (exact only where |grad sdf|=1); mesh displacement can self-intersect. + +G4 TERRAIN (holographic_terrain.py). A 2-D fBm heightfield (composition of G1) liftable to a displaced-grid +mesh (z=height, UV'd for materials) or a heightfield SDF (z-height: sign-correct, marchable). Roughness +tracks persistence (0.171 -> 0.198). The transect's box-counting dimension reads ~0.96 (a near-curve) -- +NOT a 1AB,B->A gives exactly Fibonacci generation lengths), a 3D turtle (F/+/-/&/^/\//[/]) +emitting a skeleton, scenegraph assembly (each strut instanced through a transform -- a recursive bundle +that scene_to_recipe turns back into a holographic recipe), plus recursive greeble subdivision. Productions +are themselves a holographic record sum_s bind(sym_s, exp_s) (a rule recovers at cosine 0.62). Branching +fills the plane measurably (skeleton box-counting dimension 0.14 -> 0.91 with depth). KEPT NEGATIVE: +recursive composition, not a biological growth simulation; deterministic context-free. + +G6 ATTRIBUTES (holographic_attributes.py). A per-vertex/texel attribute as a RESOLUTION-INDEPENDENT FPE +field (it is a function, so baking at a coarse and a dense sampling agrees at shared points to 0.0) -- plus +a light additive raster store (.data dict on the mesh) for hard masks. KEPT NEGATIVE: field path is +band-limited (hard 0/1 masks smooth; use the raster store). + +Wired as six UnifiedMind faculties (procedural_noise, material, displace/bump, terrain, lsystem, +attribute_field), each delegating with locally-scoped imports. Files: the six modules, their six +test_*.py (35 tests), five integration tests in test_integration.py (noise-displaces-a-field-with-exact- +undo, material-composes-with-geometry, terrain-lifts-and-takes-a-material, grown-plant-becomes-a-recipe, +attribute-field-resolution-independent), README counts, NOTES, tour. + +================================================================================ +The demoscene layer: a 3D SDF/shader algebra + procedural objects, fractals, +greebles, and vegetated terrain (S1-S2) [+21 tests: 1664 -> 1685] +================================================================================ + +DE-DUP FIRST (the rule earning its keep again): holographic_field.py ALREADY carries the demoscene +lineage -- but its `Field` lives on the VSA HYPERSPHERE (it unit-normalizes every point, measures +geodesic arccos(cosine) distance) -- the right space for "SDF = brain value = density", the WRONG space +for geometry you raymarch. So the new work is the CARTESIAN sibling, not a duplicate: signed-distance +fields over R^3 with the same operator family (union, smooth-union, domain warp/repeat). + +S1 SDF/SHADER ALGEBRA (holographic_sdf.py). A 3D signed-distance EXPRESSION TREE, one uniform `SDF(kind, +params, children)` node so eval / GLSL / DSL / holographic-tree all dispatch on `kind`. It serves four +masters at once: + * EVALUABLE -- node.eval(P:(N,3)) is vectorized numpy, so the engine's EXISTING mesh_from_sdf / + marching_tetrahedra_vec renders any tree to a watertight mesh (brain = authoritative SDF, browser = + the muscle that raymarches -- as above, so below). Primitives sphere/box/torus/cylinder/plane; + exact CSG union(min)/intersect(max)/subtract; IQ polynomial smooth-union (creaseless: measured + seam curvature 0.003 vs 0.025 hard, ~8x); transforms translate/scale(d*s)/rotate; DOMAIN REPETITION + (finite kernel -> infinite field, tiles exactly); round/onion shells; twist/displace domain warps. + * REPRESENTABLE -- to_tree() folds params into the op name ('sphere(1.0)') giving the exact + (op, child0, ...) shape typed.tree_to_recipe already encodes, so a shader IS one holographic recipe + vector (rode the EXISTING tree encoder -- no new recipe machinery). + * INPUT/OUTPUT -- to_dsl()/parse_dsl() round-trip a compact s-expression (parse(emit(t)) evals + identically); to_glsl() emits a COMPLETE Shadertoy fragment shader (helper fns + map() + raymarch + + calcNormal + lighting), built by a recursive emitter that threads a point-variable through + transforms. The shader EMBEDS its own DSL in a header comment, so a shader reads back to a tree. + * MENGER is a first-class recursive fractal primitive (box minus crosses at every scale): it evals, + marches, AND emits a real GLSL for-loop helper -- the canonical demoscene fractal in the algebra. +KEPT NEGATIVES: union/intersect/subtract are exact, smooth-union is the standard bounded approximation, +and twist/displace are domain warps that BREAK unit-gradient (bounded Lipschitz fields, not true +distances) -- the emitter flags them ("shorten ray steps") rather than pretend. Non-uniform scale is +omitted (it does not preserve a distance field). GLSL is emit-only; the editable canonical form is the +DSL/tree (a shader reads back via its embedded DSL, NOT by parsing arbitrary GLSL). + +S2 PROCEDURAL GENERATION (holographic_procgen.py). The composition layer turning S1 + G1-G6 into the +three asks: (1) `procedural_object(seed)` -- a random SDF tree (Quilez's tiny-seed-to-world), deterministic +per seed, renders + emits + represents; (2) FRACTAL/GREEBLE models -- menger re-exported, plus +`greeble_mesh(base, seed)` lifting the G5 flat-panel greeble onto ANY mesh's faces (centroid + normal, +extruded boxes, merged); (3) `scatter_on_terrain` / `vegetated_terrain` -- L-system plants (G5) instanced +across a fBm terrain (G4) at the surface height with per-instance jitter, into one scenegraph. Measured: +object marches to 5940 faces, menger(2) to ~150k faces (the holes ARE the surface), greeble 8->56 verts, +scatter lands every instance at terrain height exactly (1e-9). KEPT NEGATIVES: procedural_object is a +generator not an art director (a random subtract can erase most of itself / leave disconnected pieces); +greeble is instancing not CSG (greebles intersect the hull -- which is how greebling looks); scatter is a +deterministic placement, not an ecology. + +Wired as seven UnifiedMind faculties: sdf_object, sdf_render, sdf_shader, sdf_parse, menger_fractal, +greeble, vegetated_terrain. Files: the two modules, test_holographic_sdf.py (10) + test_holographic_procgen.py +(7), four integration tests (sdf-object-renders-and-round-trips-dsl-and-recipe, sdf-shader-shadertoy-ready, +menger-and-greeble, vegetated-terrain), README counts, NOTES, tour. + +================================================================================ +Bridges from the procedural/SDF layer to the rest of the stack -- MEASURED +(compression, the soft operator, denoising, structure) [+7 tests: 1685 -> 1692] +================================================================================ + +The question: do the recent SDF/procedural changes (S1-S2) UNLOCK anything elsewhere? We measured every +candidate on the real substrate BEFORE building (the engine's own rule), and the answer split cleanly +into two wins and two negatives/already-dones. All four are kept loud in holographic_procbridge.py, +because a connection ruled out by measurement is as valuable as one ruled in. + +C1 -- COMPRESSION / COMPLEXITY (WIN, wired as `procedural_compression`). A procedural generator's size is +CONSTANT in its output's complexity: a Menger sponge's DSL is 12 BYTES whether it marches to 100k or 250k +faces (measured ratios 130,000-500,000x). Storing the GENERATOR instead of the expanded geometry escapes +the capacity/complexity wall for any content that HAS a short generator -- the SAME MDL principle as +symbolic_regress/compress_signal ("find the law, store the law"), now for geometry, and the same spirit +as domain repetition (an unbounded tiled field from one bounded cell). KEPT NEGATIVE: only COMPRESSIBLE +content has a short generator; an arbitrary scanned/random mesh does not (the symbolic-regression "not +everything has a law" negative). Procedural compression is lossy and content-restricted, not a universal +codec. + +C4 -- THE SOFT OPERATOR (WIN, a unification, wired as `soft_min`). The SDF smooth-union and the engine's +memory cleanup are the SAME temperature-controlled soft operator. Measured: smooth_union(k->0) converges +to the hard union exactly (gap 0.125 -> 0.0025 as k: 0.5 -> 0.01), precisely as the modern-Hopfield/softmax +cleanup at beta->inf becomes the hard nearest-neighbour (a celebrated engine fact). `soft_min(a,b,k) = +-k*log(exp(-a/k)+exp(-b/k))` is the log-sum-exp form -- the SAME log-sum-exp softmax uses: softmax is a +soft-arg-MAX, soft_min is a soft-arg-MIN over distances, with k = 1/beta. A smooth blend of geometry and a +soft recall of a memory are one piece of math seen in two domains. (Practical payoff is modest -- it is a +unification, not a speed-up -- which is why it ships as one measured fact + a shared primitive, not a rewrite.) + +C2 -- FPE FIELD AS A DENOISER (NEGATIVE, kept; NOT wired into `denoise`). An FPE field fit to noisy samples +is a kernel/RBF regressor (Nadaraya-Watson). Measured: it HURTS on uniformly-sampled smooth signals +(SNR 7.7 -> 2.5 dB; it over-smooths), rounds sharp edges badly (9.7 -> 3.0 dB), and on non-uniformly sampled +signals it is at best marginal and SEED-DEPENDENT -- not a reliable win. The shipped trajectory/SSA and +spectral denoisers dominate. So it is deliberately NOT added to the denoise faculty (it would degrade it); +`fpe_smooth` is kept only as the honest record of the attempt and a scattered-data smoother for a caller +who explicitly wants one. The negative is locked into the test suite (test asserts it does NOT beat the +noisy signal on uniform sampling). + +C3 -- SDF SCENE AS A FACTORABLE STRUCTURE (ALREADY DONE). An SDF tree IS a typed.tree_to_recipe recipe, so +decode_structure / decompose_structure / op_kinds already read its structure back -- nothing to build, +noted so the panel does not "discover" it again (the recurring denoising-session lesson: it was already in +the box). + +Wired: two UnifiedMind faculties (procedural_compression, soft_min); fpe_smooth stays a module function +(the kept negative). Files: holographic_procbridge.py, test_holographic_procbridge.py (6), one integration +test (procedural-compression-and-soft-operator-through-the-mind), README counts, NOTES, tour. + +================================================================================ +Substrate Evolution + Differentiable Orchestration: the harmonic codebook becomes +self-organizing, tool-chains become composable & optimized [+8 tests: 1692 -> 1700] +================================================================================ + +Two requested upgrades, both elevating something to be first-class and composable in VSA programs, both +NumPy-only and gradient-WITHOUT-autodiff (the engine's standing rule). + +SUBSTRATE EVOLUTION (holographic_harmonic.py -> OnlineHarmonicAtom; faculty `evolving_atom`). harmonic_atom +fits a context-conditioned meaning atom ONCE, by np.linalg.lstsq over (context-angle, meaning) samples in a +circular-harmonic basis. The evolution step makes that fit AUTONOMOUS: Recursive Least Squares is the EXACT +online form of the same least-squares solution -- each observation updates the coefficient matrix W and the +inverse-covariance P by a rank-1 Sherman-Morrison step, no refit, no stored history. So the "codebook" stops +being a frozen table and becomes a self-organizing dynamical system. + * MEASURED: with forgetting=1.0 the online stream CONVERGES to the batch lstsq fit (max coeff gap 1.6e-7). + * MEASURED: with forgetting<1.0 it down-weights stale evidence and TRACKS a drifting meaning function -- + decode error on the CURRENT function 0.43 (tracking) vs 2.24 (no-forgetting, still trusting old data). + * Deterministic given the observation stream; it is the engine's own least squares run online, not a + backprop-learned weight. KEPT NEGATIVE: forgetting<1 trades steady-state accuracy on a STATIONARY + function for the ability to follow a non-stationary one -- on stationary data, forgetting=1 is better. + +DIFFERENTIABLE ORCHESTRATION (holographic_orchestrator.py -> optimize_toolchain / Planner.plan_differentiable; +faculty `optimize_toolchain`). The orchestrator already maps tools into hyperspace (every Tool has a .vec), +and plan/score rank them by an INDEPENDENT per-tool cosine. The differentiable upgrade optimizes a WHOLE +chain JOINTLY against a chain-level structural score. A chain's signature is the order-encoded superposition +sum_s permute(tool_s.vec, s) (the engine's permute = np.roll; order-sensitive). Given a goal signature (what +a working composed chain should look like), a SOFT selection -- a softmax distribution over tools at each of +L steps -- is optimized by gradient ASCENT on cosine(chain_signature, goal_sig). The gradient is derived +ANALYTICALLY through cosine -> superposition -> permute -> softmax, in numpy, NO autodiff (the same +gradient-without-a-framework method as holographic_optimize). argmax of the converged soft selection gives +the discrete chain of real Tools. + * MEASURED: on a CORRELATED tool set (tools sharing a common component, where independent scoring is misled + by cross-talk between positions) the optimizer recovers the true ordered chain (4/4, composed cosine + 1.000) while position-blind per-tool greedy scores 0.853. Deterministic. + * KEPT NEGATIVE: gradient ascent finds a LOCAL optimum of a non-convex landscape; on ORTHOGONAL/easy tool + sets per-position greedy already recovers the chain (no gain) -- the win is specifically the correlated / + interacting-positions regime. It optimizes against a SUPPLIED goal signature (from a demonstrated chain), + not an end-task reward. + +The connective tissue (the first ask, "more things composable in VSA programs"): the harmonic codebook is +now a live self-organizing atom rather than a static fit, and the tool registry's chains are now a +differentiable object optimized in the same hyperspace as everything else -- both elevated to first-class, +composable, evolving citizens of a VSA program. The optimizer's softmax-over-scores is the SAME soft +operator the last session unified (C4): a soft tool-selection and a soft memory recall are one piece of math. + +Wired: two UnifiedMind faculties (evolving_atom, optimize_toolchain); OnlineHarmonicAtom + optimize_toolchain/ +chain_signature/Planner.plan_differentiable in the two modules. Files: holographic_harmonic.py, +holographic_orchestrator.py, their two test files (+6), two integration tests (evolving-atom-converges, +differentiable-toolchain), README counts, NOTES, tour. + +================================================================================ +The creature's value head AS a VSA program -- policy = hypervectors, learn = +bundling, decide = a dot (measured head-to-head vs the tabular brain) [+7: 1700 -> 1707] +================================================================================ + +THE DIAGNOSIS. The creature mind (HolographicMind) is holographic on the OUTSIDE -- states are role-bound +bundles, prototypes are superpositions, perceive is bundle-and-cosine, describe/why_differ are real +unbind+cleanup -- but TABULAR on the inside. For each action it keeps a GROWING list of prototype direction +vectors (self._unit[a]) paired with a PARALLEL NUMPY ARRAY OF SCALAR mean-returns (self._ret[a]); value(s,a) +is a numpy kernel-weighted average of those scalars and _absorb is "classify to nearest prototype, nudge its +scalar return, or vstack a new one." That scalar-return table is the one part of the brain that is not itself +a VSA program -- the reason the creature "functions a little different" from the recipes / SDF trees / +orchestrator that were all elevated to LIVE in the holographic space. + +THE TRANSFORMATION (holographic_valuehead.HolographicValueHead; faculty `holographic_value_head`). The +creature's value is exactly a Nadaraya-Watson estimator, value(s,a)=sum_i sim(s,proto_i)*ret_i / sum_i +sim(s,proto_i), and a sum is a BUNDLE. So keep, per action, just two hypervectors: + Q_a = sum_i ret_i * unit(state_i) N_a = sum_i unit(state_i) +and because IS the cosine, value(s,a) = / reproduces the SAME average -- but +the whole per-action policy is TWO fixed-length vectors instead of an unbounded list, learning is one +bundling step (Q_a += ret*u ; N_a += u -- the holographic complement, O(1) and history-independent), and the +policy {Q, N} is a savable / bindable / composable hypervector program. Same drop-in API as the brain: +value(state, action)->(value, support) and absorb(state, action, ret). + +MEASURED HEAD-TO-HEAD against the REAL tabular brain (same experience stream through both, value mechanism +isolated -- no consolidation/projection): + * LOW load (P=8 distinct situations, D=512): holo MATCHES tabular -- best-action accuracy 0.88 vs 0.88, + value RMSE 0.05 to the true value. The two-bundle head reproduces the brain's decisions. + * HIGH load (P=500 ~ D): holo DEGRADES to 0.52 (toward chance) while the tabular table stays at 0.93 -- + the VSA CAPACITY CLIFF. KEPT NEGATIVE: the scalar table is effectively exact and grows without bound; + folding everything into 2 vectors per action blurs under cross-talk past ~D situations. The win is one + composable, FIXED-SIZE (24,600 B at BOTH loads), gracefully-degrading hypervector policy that is + consistent with the rest of the stack -- NOT higher small-scale accuracy. + * Two more kept caveats: the head's `support` is the similarity MASS (a sum), not the tabular + NEAREST-prototype similarity (a max) -- a different, mass-based familiarity it cannot cheaply turn into + "nearest" without the list it discards; and it uses a LINEAR kernel (the table clips negative sims and + keeps k-nearest), mostly harmless in high dimensions where dissimilar states are near-orthogonal. + +WHERE THIS SITS. This is the value HEAD made holographic, measured before any surgery on the live brain +(backward-compatible by construction -- the tabular brain is untouched and still the default). The clean +follow-ups, in order: (1) wire it into HolographicMind.decide/_absorb behind a value_backend='holo' flag and +re-run the head-to-head on the creature's actual maze/world task; (2) lean the high-load case on the routing +fabric / sparse codes / resonator to push the cliff back (the honest capacity remedy); (3) TD bootstrapping +as VSA (n-step returns as discounted bundles, eligibility traces as a decaying bundle) -- the genuine frontier. + +Wired: faculty holographic_value_head; module holographic_valuehead.py; test_holographic_valuehead.py (6) + +one integration test (value-head-is-a-savable-policy); README counts, NOTES, tour. + +================================================================================ +Holographic value backend WIRED into the live creature (value_backend='holo'): +the whole brain runs on a hypervector policy [+4 tests: 1707 -> 1711] +================================================================================ + +The previous step built HolographicValueHead standalone and measured it on synthetic situations. This step +WIRES it into HolographicMind behind a flag and re-runs the comparison on the creature's REAL task. + +THE WIRING (holographic_creature.py). A new constructor flag value_backend='table' (default, BIT-IDENTICAL) +| 'holo'. In 'holo' mode the brain instantiates a HolographicValueHead(dim, n_actions) and value() / +_value_projected() / _absorb() return through it -- so decide()'s scoring loop, exploration, vetoes, and the +corridor reflex are all UNCHANGED; only the value storage/recall swaps from the growing prototype table to +the two-bundle hypervector policy. The head is fixed-size, so the consolidation/projection machinery is +simply unused in this mode. Default 'table' is untouched and still the default (its own tests, incl. the +maze gauntlet, pass unchanged). + +MEASURED HEAD-TO-HEAD on the creature's actual GridWorld mazes (same enc/seed/episodes, only the backend +differs), trained 150 episodes, escape rate over 20 eval runs: + * 7x7 maze (seed 3): table 100% escape, policy 345,408 B (grows) | holo 100% escape, policy 16,416 B FIXED. + * 9x9 maze (seed 5): table 100% escape, policy 275,504 B (grows) | holo 100% escape, policy 16,416 B FIXED. +So on the real task at this scale the holographic backend TIES control performance (100% vs 100%) while the +policy is a fixed ~16 KB hypervector program {Q, N} -- ~17-21x smaller than the table and, unlike the table, +CONSTANT regardless of maze size / experience count. The capacity cliff measured earlier (synthetic, P~dim) +is where it would degrade: a world with far more distinct junction-states than the dimension can hold. At +maze scale (dim 256, a few hundred experiences) the brain sits well below the cliff, so the win is clean: +same escape, a tiny fixed savable policy, and the creature's one tabular part now lives in holographic space. + +HONEST SCOPE. This validates the backend at maze scale; it does NOT claim a win at all scales (the cliff is +real and on record). support is still mass-based not nearest-based (the novelty bonus uses it but escape was +unaffected here). The remaining frontier is unchanged: push the cliff back with the routing fabric for very +large worlds, and TD bootstrapping as VSA. + +Wired: HolographicMind.value_backend flag; value/_value_projected/_absorb routed; test_creature_holo_backend.py +(4 -- default-unchanged, routing, fixed-size policy, learns-a-real-maze); README counts, NOTES, tour. + +================================================================================ +Holographic creature: cliff pushed back (routing), TD as VSA, usable everywhere, +and composable into other VSA programs [+7 tests: 1711 -> 1718] +================================================================================ + +Four moves on the holographic value head, in the order they were asked. + +STEP A -- ROUTING PUSHES THE CAPACITY CLIFF BACK (RoutedValueHead; value_backend='routed'). A single (Q_a, +N_a) pair blurs past ~dim distinct situations. RoutedValueHead routes every state to one of B buckets by a +fixed random-projection sign hash (LSH -- the same mechanism as HoloForest / the RP trees) and keeps a +(Q, N) per bucket, so each bundle holds only the situations that hash together -- bounded load. MEASURED at +1024 situations (4x dim=256): single-bundle 0.39 -> routed(64 buckets) 0.89 best-action accuracy; at the +cliff (256) 0.57 -> 0.94. KEPT NEGATIVE: B-fold memory, and a query reads only its own bucket so two similar +states on opposite sides of a hash plane miss each other (boundary smoothing). 'cull, don't batch' for value. + +STEP B -- TD AS VSA (discounted_return, EligibilityTrace). Learning stays a bundling step; only the TARGET +changes from a realised return (Monte-Carlo) to a bootstrapped one (TD). An n-step return is a DISCOUNTED +BUNDLE of rewards plus a bootstrap (sum gamma^k r_k + gamma^n V), and the eligibility trace is a DECAYING +BUNDLE of recent states (e <- gamma*lambda*e + unit(state)) -- both first-class. MEASURED on the canonical +random-walk value prediction (Sutton & Barto 6.2): after 40 episodes TD RMSE 0.085 < MC RMSE 0.134 -- +bootstrapping converges with lower error (lower variance). The bootstrap V(s') is read from the head, so the +whole TD loop stays in the holographic space. + +USABLE EVERYWHERE (UnifiedMind.actions(value_backend=...), use_holographic_brain()). The creature is held as +self._brain and used by decide/reinforce; actions() now takes value_backend='table'|'holo'|'routed', and +use_holographic_brain(routed=) swaps an existing brain to a hypervector policy in place. So anywhere the +creature is used inside the engine, the holographic creature can be used instead -- decide/reinforce unchanged. + +COMPOSABLE INTO OTHER VSA PROGRAMS (policy_atom, decide_from_atom, from_policy). The policy folds into TWO +bindable hypervectors -- M_Q = sum_a bind(code_a, Q_a), M_N = sum_a bind(code_a, N_a) -- so it is one object +that can be bound into a recipe/structure and carried around the VSA space. decide_from_atom drives a choice +straight from {M_Q, M_N, action codebook, state} by unbind+dot -- a decision made INSIDE the VSA program, no +trip back through Python (the slow boundary). MEASURED: atom-driven decisions match the head 4/4 within +capacity; from_policy round-trips a saved policy exactly. KEPT NEGATIVE: folding all actions into two +D-vectors adds cross-talk, so atom-driving matches only within capacity (few actions / large dim). + +WHY IT MATTERS (the performance point). Crossing Python<->VSA is the expensive boundary; everything here keeps +the decide/learn loop and now the policy itself as array ops / hypervectors, so a VSA program can carry and +drive the creature without round-tripping. The remaining boundary is perception (senses dict -> encode); a +fully in-VSA perceive is the next frontier, alongside routing the live maze brain for very large worlds. + +Wired: RoutedValueHead, discounted_return, EligibilityTrace, policy_atom/decide_from_atom/from_policy in +holographic_valuehead.py; value_backend='routed' in HolographicMind; actions(value_backend=) + +use_holographic_brain + routed faculty in UnifiedMind. Tests: +5 valuehead, +1 creature-backend, +1 +integration. README counts, NOTES, tour. + +================================================================================ +Compiled, fully in-VSA perception: the creature loop becomes array ops end-to-end +(FastCreatureEncoder) [+5 tests: 1718 -> 1723] +================================================================================ + +THE LAST BOUNDARY. With the holographic value head, decide is a dot and learn is a bundle -- but PERCEPTION +still did an FFT bind (a convolution) per sense feature EVERY step, recomputing the same role/filler bind +whenever a feature recurred. That per-step convolution was the last expensive Python<->VSA crossing in the +creature loop. + +THE FIX (FastCreatureEncoder, subclass of CreatureEncoder; faculty fast_creature_encoder). Cache each bound +atom the first time its (role, value) is seen; thereafter perception is a GATHER + bundle (a sum) -- pure +array ops, no per-step FFT. Because it caches the EXACT same scalar bind and bundles in the same sorted +order, the output is BIT-IDENTICAL to the plain encoder (only the redundant convolutions are skipped). Kept +an opt-in subclass so the tie-sensitive rescue canary keeps the plain encoder by default. +perception_codebook() exposes the cached atoms as one (features, dim) matrix -- the in-VSA form of the senses +dict (perceive = indicator @ matrix). + +MEASURED: + * Bit-identical: np.array_equal to CreatureEncoder.encode on every tested sense set. + * Speed: 4000 perceptions 537ms -> 65ms (8.3x); 7-8 FFT binds done at warm-up, ~12000 avoided -- steady + state is 0 FFTs per step. + * Full in-VSA loop: compiled perceive (gather+sum) + ROUTED hypervector brain (decide=dot, learn=bundle) + learns the 7x7 maze to 100% escape -- the whole perceive->decide->learn loop is array ops, the creature a + VSA program end-to-end, with the routing fabric giving capacity headroom for large worlds. + +WHY IT MATTERS. Moose's performance point: crossing Python<->VSA is the expensive boundary, and things inside +VSA are faster. The loop now stays inside: perception is a gather, decision a dot, learning a bundle, the +policy a pair of bindable hypervectors. The remaining per-step Python is just the world's senses dict and the +episode control flow (the environment, not the mind). + +Wired: FastCreatureEncoder + perception_codebook in holographic_creature.py; fast_creature_encoder faculty in +UnifiedMind. Tests (focused only -- the full suite is left to CI per request): test_creature_fast_perceive.py +(4) + one integration test. README counts, NOTES, tour. + +================================================================================ +Vectorized VSA-program primitives: the common ops in one array op, not a Python +FFT loop [+6 tests: 1723 -> 1729] +================================================================================ + +AUDIT FIRST (the honest finding). Of the operations VSA programs run constantly, cleanup was ALREADY +vectorized -- Vocabulary.cleanup is a cached matrix @ query + argmax, not a Python cosine loop. The batched +FFT primitives bind_batch / bind_fixed ALSO already existed. What was missing was the convenience layer over +them for the two most common patterns, so encoders and decoders still hand-looped FFTs: + * record/structure encoding: bundle([bind(role_i, val_i) for i]) -- K FFT binds per call. + * multi-key decode/resonate: [unbind(trace, k) for k in keys] -- N FFT unbinds per call. + +ADDED (holographic_ai.py, the core, importable by every program): + * involution_batch(K) -- involution over a stack in one op. + * unbind_all(trace, keys) = bind_fixed(trace, involution_batch(keys)) -- one trace unbound against many + keys in ONE batched FFT (the decode/resonator loop, vectorised). + * bundle_bind(keys, values) = bundle(bind_batch(keys, values)) -- a record/structure encoded in ONE + batched FFT (the role/filler loop, vectorised). + * nearest(query, matrix) = argmax(matrix @ query) -- the reusable matmul cleanup, EXACT (no epsilon), for + the scattered [cosine(q, v) for v in set] loops. + +MEASURED: bundle_bind and unbind_all match their scalar loops exactly here (max diff 0.0) and run ~4.4x / +~4.6x faster at K=24; nearest's argmax equals the cosine loop's. KEPT NEGATIVE: the batched FFT can differ +from the scalar bind loop at ~1e-16 -- enough to flip a knife-edge tie-break -- so tie-sensitive encoders +(the maze-rescue CreatureEncoder, which is why it uses the cached bit-identical FastCreatureEncoder instead) +keep the scalar/cached form; wide-margin encoders adopt the batched form. + +ADOPTED safely: HolographicLearner.encode (the classifier's record encoder) now runs through bundle_bind -- +classification is wide-margin, test_holographic.py (43 tests) green. (holographic_encoders.RecordEncoder +already used bind_batch.) Exposed as UnifiedMind faculties encode_record / unbind_keys / nearest_in for +programs built on the mind. CreatureEncoder is deliberately NOT switched (tie-sensitive) -- it has the cached +FastCreatureEncoder for its in-VSA speedup. + +WHY IT MATTERS. These are the operations a VSA program runs in its inner loop; doing each as one array op +(batched FFT or matmul) instead of a Python loop keeps the program inside VSA space, where it is fast. The +audit's lesson held again: some of the "missing" vectorization was already in the box (cleanup, bind_batch) -- +the real gap was the thin convenience layer that lets the common ops actually call it. + +Wired: involution_batch / unbind_all / bundle_bind / nearest in holographic_ai.py; HolographicLearner.encode +via bundle_bind; encode_record / unbind_keys / nearest_in faculties. Tests (focused, per request): +test_holographic_primitives.py (6). README counts, NOTES, tour. + +================================================================================ +Grid fields + particle/fluid simulation, exposed to VSA [+11 tests: 1729 -> 1740] +(plus the nearest pass: PartitionedMemory.route now a matmul) +================================================================================ + +THE NEAREST PASS (finishing the prior thread). PartitionedMemory.route was a Python loop +[argmax(cosine(key, a) for a in anchors)]; it now calls nearest(key, anchor_matrix) -- one matmul, EXACT +(the anchors are unit random_vectors, so argmax of the dot equals argmax of the cosine), cached anchor stack. +Verified 200/200 identical to the old loop. The remaining cosine-loops are either benchmark/selftest code or +dict-based max(d, key=...) returns in text.py (those would want a separate nearest-by-name helper and touch +text paths the focused runs don't cover, so they're left for a deliberate pass). + +THE CAPABILITY QUESTION (fields / forces / particles / deformers / sim). Audit first -- most building blocks +already existed under other names: + * divergence / curl / pressure projection -> holographic_spectral.py Hodge decomposition (gradient / + solenoidal / harmonic split) does this on graphs/meshes (discrete exterior calculus). + * density transport -> holographic_transport.py Wasserstein/Sinkhorn OT. + * flow fields -> holographic_flow.py Tero/Physarum flux on a graph. + * forces / trajectories -> holographic_physics.py Kinematics (state, acceleration, step). + * attractors -> holographic_chaos.py Lorenz + reservoir attractors. + * articulated constraints -> holographic_meshik.py FABRIK IK. + * deformers -> holographic_sdf.py twist / displace / domain warps. +THE GAP: no exposed REGULAR-GRID Eulerian fluid/particle layer -- velocity/pressure/density/temperature grids +with advection, and a particle system with forces. That is the deepest VSA-native gap, because the FFT-on-a- +torus IS the bind operator (Jos Stam's Stable Fluids uses the exact same transform to diffuse and project). + +BUILT holographic_fields.py: + * diffuse(field, amount) -- Gaussian heat kernel via FFT = a bind with a Gaussian kernel on the + torus; mass conserved exactly (DC gain 1). MEASURED: var 0.020 -> 0.013, mean unchanged. + * divergence / curl -- spectral i*k first derivatives. + * project_divergence_free(vx, vy) -- the PRESSURE PROJECTION (Helmholtz, FFT): remove the gradient part so + div ~ 0. MEASURED: max|div| 7.81 -> 2.5e-15. Idempotent. + * advect(field, vx, vy, dt) -- semi-Lagrangian backtrace, periodic bilinear. MEASURED: a blob moves + exactly v*dt (~6 cells). + * fluid_step(...) -- the Stable-Fluids loop (force -> diffuse -> project -> self-advect -> + project -> advect density). MEASURED: stays incompressible (max|div| ~1e-15) while transporting density. + * ParticleSystem / attractor_force / sample_field -- particles feel forces and RIDE the solved velocity + field (sample it bilinearly). MEASURED: an attractor pulls particles 12.6 -> 9.5; a flow carries them. + +THE BUG WE FIXED (worth recording). The projection at first only knocked divergence 7.8 -> 1.07, not -> 0. +Root cause: with plain fft2/ifft2 the projected spectrum was not Hermitian-symmetric, so real() corrupted it; +and even with rfft2/irfft2 the residual was ENTIRELY the Nyquist row/column -- a FIRST derivative of the +Nyquist cosine is undefined on an even grid (no matching sine). Fix: use rfft2/irfft2 (guaranteed real +inverse) AND zero the Nyquist modes in the first-derivative wavenumbers, building k^2 from those same zeroed +wavenumbers so the projection's k.v_new cancels to machine zero (diffusion, being even-order, keeps the full +k^2). Result: 7.81 -> 2.5e-15. Lesson logged: prefer rfft2/irfft2 for real-field spectral ops, and zero the +Nyquist for odd-order derivatives on even grids. + +KEPT NEGATIVES: the grid is a periodic torus (no solid walls -- which is exactly why the FFT applies); +semi-Lagrangian advection is numerically diffusive (sharp features smear -- the Stable-Fluids stability +trade); FFT diffusion is one global isotropic amount per step (no heterogeneous viscosity). Softbody/hardbody +PBD/XPBD and a full deformer set (bend/taper/FFD lattice) are NOT built -- the constraint-projection pieces +exist in spirit (IK, Hodge denoise, resonator-as-projection) but a physical position-based-dynamics solver is +future work. + +Wired: holographic_fields.py (diffuse/divergence/curl/project_divergence_free/advect/fluid_step/ +ParticleSystem/attractor_force/sample_field); UnifiedMind faculties diffuse_field / make_incompressible / +field_divergence / advect_field / fluid_step / particle_system / attractor_force / sample_field; +PartitionedMemory.route via nearest. Tests: test_holographic_fields.py (9), the route test in +test_holographic_primitives.py (+1), one integration test (+1). README counts, NOTES, tour. + +================================================================================ +PBD/XPBD softbody + shape-matching hardbody, exposed to VSA [+9 tests: 1740 -> 1749] +================================================================================ + +DE-DUP FIRST. The iterate-a-projection sweep ALREADY exists: holographic_denoise.project_onto_constraints +(the mind's `project_onto_constraints` faculty) -- Macklin's observation that the SBC resonator, the PnP +denoiser, IK, and a position-based-dynamics constraint sweep are the SAME object. IK (holographic_meshik) is +built on it. What did NOT exist was the DYNAMICS around the sweep: momentum, inverse mass, gravity, the +predict -> solve -> velocity-update time-step, time-step-independent stiffness, collision. So this is a +genuine extension, not a re-implementation -- and the PBD path delegates its sweep to the shipped engine. + +BUILT holographic_softbody.py: + * SoftBody -- particles + distance constraints, PBD/XPBD time-stepping. inv_mass 0 PINS a particle (the + hardbody anchor of a soft sheet). Builders: rope(n), cloth(rows, cols). External per-particle force input + so the FIELD layer can push it. + * Two solver back-ends: + - solver='pbd' delegates the constraint sweep to the shipped project_onto_constraints (the same way IK + builds bone projections and hands them over -- the unification made literal). + - solver='xpbd' adds per-constraint COMPLIANCE with an accumulated Lagrange multiplier -- the piece the + generic sweeper does not carry -- giving time-step/iteration-independent stiffness. + * RigidBody -- a hardbody via SHAPE MATCHING (Mueller 2005): each step, the optimal rotation mapping rest -> + current is the polar decomposition (an SVD) of the mass-weighted cross-covariance; particles are pulled to + that rigid goal. Falls and rotates, never deforms. + +MEASURED (selftest + tests): + * distance constraint converges: residual -> 0 (1e-16). + * XPBD stiffness is TIME-STEP INDEPENDENT (the headline): a hanging spring's static stretch = compliance*g + = 0.098 for compliance 0.01, IDENTICAL at 1 vs 6 substeps once settled. Derivation: the predicted gravity + drift per step is h^2*g and XPBD's alpha~=compliance/h^2, so the two h^2 cancel and the elongation is h- + independent -- confirmed empirically. + * cloth reaches equilibrium under gravity + pinned top row (residual < 0.05). + * STABLE at dt=0.1 -- a time-step that explodes an explicit spring -- positions stay bounded (max|x| ~4). + * PBD-via-the-shipped-sweeper also satisfies the constraint (engine reuse works). + * rigid body stays rigid (distance drift 1e-16) while falling under gravity. + * VSA COUPLING: an attractor force from holographic_fields pushes a soft body toward the attractor while it + stays intact -- field forces driving a softbody, one substrate. + +KEPT NEGATIVES (honest): + * Plain PBD's effective stiffness depends on the iteration count; only XPBD (compliance) is iteration/time- + step independent. Both shipped; the difference is the reason XPBD exists. + * Measuring the XPBD static stretch needs the motion to SETTLE -- undamped, the substep-6 case keeps gently + oscillating (the velocity differs, not the stiffness); a little damping reaches true static equilibrium. + * Gauss-Seidel sweep is ORDER-dependent (kept sequential for determinism); collision is a simple floor half- + space with restitution -- no self-collision, no friction; bending/volume constraints not built (the next + constraint type for the same sweep). + +WHY IT MATTERS / THE TIE-IN. PBD is the PHYSICAL face of the engine's iterate-a-projection pattern -- the same +operation as the resonator (project onto factor codebooks), the PnP denoiser (project onto the signal +manifold), and IK (project onto bone lengths), now carrying momentum. And because a body takes an external +per-particle force, the fluid/field layer (holographic_fields) drives it -- cloth in wind, a soft body in an +attractor well -- on the one shared substrate. + +Wired: holographic_softbody.py (SoftBody, RigidBody, rope/cloth builders, pbd+xpbd solvers); UnifiedMind +faculties soft_body / cloth / rope / rigid_body. Tests: test_holographic_softbody.py (8) + one integration +test (+1). README counts, NOTES, tour. + +================================================================================ +Bending + volume constraints, and two-way fluid<->cloth coupling [+6 tests: 1749 -> 1755] +================================================================================ + +Extends the PBD/XPBD softbody and the fluid layer so cloth resists FOLDING, soft solids hold their VOLUME, +and the fluid and a cloth push EACH OTHER. + +BENDING (SoftBody.add_bending, cloth3d's `bending=`). Implemented as a BEND SPRING -- a distance constraint +between the two corners two cells apart across a fold line (Provot's classic cloth bending). Folding changes +that separation, so holding it resists the fold. KEPT NEGATIVE / honest choice: the dihedral-ANGLE constraint +(Mueller 2007) is exact but SINGULAR at the flat rest state (acos' derivative blows up at d=+-1), so the bend +spring is the robust model used; _dihedral() is kept only to MEASURE fold. MEASURED cleanly on a 3-particle +strip (distance constraints are happy at any fold angle, so this isolates bending): a V folded to gap 1.53 is +flattened back to 2.00 with a bend spring, stays 1.53 without. (A draping cantilever is a confounded metric -- +without bending a sheet CRUMPLES near the pin; the clean test is the forced-fold-then-release above.) + +VOLUME (SoftBody.add_volume, soft_box). The PBD tetrahedron volume constraint (Mueller 2007): C = V - V0 with +V = (1/6)(j-i).((k-i)x(l-i)); gradients are cross products of the tet edges; solved with the same XPBD +compliance/multiplier machinery. MEASURED: a single tet squashed (apex pulled in) recovers its volume 0.167 +-> 0.167; a 3x3x3 soft_box (cube cells split into 5 tets each) preserves total volume 5.333 under a squash of +its top layer. A jelly block that springs back, not a collapsing sheet. + +TWO-WAY FLUID <-> CLOTH COUPLING (holographic_fields). + * scatter_to_field(shape, positions, values) -- the ADJOINT of sample_field: spread per-particle values onto + the grid bilinearly (np.add.at over the four nearest cells). Mass-preserving; exact round-trip with + sample_field at a cell. This is how a body IMPRINTS momentum into the fluid (cloth -> fluid). + * drag_force(positions, velocities, vx, vy, k) -- F = k(v_fluid - v_particle), the fluid pushing the body + (fluid -> cloth). + MEASURED: a clump moving at +5 in x, scattered into the fluid's force grids and stepped, leaves the fluid + with max|vx| ~2.1 (the body stirred the fluid); a uniform +4 flow drags free particles' mean vx up from 0 + toward 4. Both directions, on the one grid the bind operator's FFT already runs. + +WHY IT MATTERS. These are the two missing PBD constraint TYPES (bending, volume) plus the reverse of the +sampling primitive (scatter), which together close the loop: the fluid layer and the softbody layer are now +genuinely coupled -- wind fills a sail, a swimming body stirs the water -- all bind/bundle/FFT on one +substrate. KEPT NEGATIVES: bend spring (not dihedral angle) for non-singularity; Gauss-Seidel order-dependence +stays; two-way coupling here is momentum exchange via scatter/sample (not a pressure-accurate immersed +boundary); periodic torus (no walls). + +Wired: SoftBody.add_bending / add_volume / cloth3d / soft_box / total_volume; holographic_fields +scatter_to_field / drag_force; UnifiedMind faculties cloth3d / soft_box / scatter_to_field / drag_force. +Tests: +3 softbody (bending, volume, soft_box), +2 fields (scatter adjoint, drag), +1 integration (two-way +coupling). README counts, NOTES, tour. + +================================================================================ +Smoke: temperature -> buoyancy + vorticity confinement [+5 tests: 1755 -> 1760] +================================================================================ + +Completes the TEMPERATURE FIELD from the original capability question by coupling it to velocity -- a real +smoke/convection sim on the existing FFT fluid solver. Fedkiw, Stam & Jensen (2001), "Visual Simulation of +Smoke". + +BUILT (holographic_fields): + * buoyancy_force(temperature, density, alpha, beta, ambient) -- Boussinesq: hot fluid RISES (a +y force + proportional to temperature above ambient), heavy smoke SINKS (a -y force proportional to density). + Returns (fx=0, fy). This is what turns a static temperature field into motion. + * vorticity_confinement(vx, vy, epsilon) -- semi-Lagrangian advection numerically damps small vortices + (smoke goes mushy); this adds f = epsilon*(N x w), N = grad|w|/|grad|w|| pointing toward higher vorticity, + restoring the curl the advection lost. Larger epsilon = curlier. + * smoke_step(vx, vy, density, temperature, ...) -- inject sources -> buoyancy + confinement on velocity -> + diffuse -> project -> advect, carrying BOTH density and temperature. The full smoke loop. + +MEASURED: a hot blob's smoke centroid rises row 10 -> 29 (buoyancy lifts it); vorticity confinement keeps +total |w| ~3x higher than without (369 -> 1216) -- visibly curlier; a hot+dense source at the bottom builds a +rising plume (centroid well above the source row, density accumulating). All on the same FFT-on-a-torus the +bind operator runs. + +KEPT NEGATIVES: 'up' is +y (+row) by convention (the periodic grid has no gravity of its own); confinement is +a heuristic force (it injects energy -- too-large epsilon makes the flow noisy); still the periodic torus (a +plume wraps if it reaches the top); semi-Lagrangian advection still diffuses density over long runs (the +confinement only restores velocity vorticity, not density sharpness). + +Wired: holographic_fields buoyancy_force / vorticity_confinement / smoke_step; UnifiedMind faculties +smoke_step / buoyancy_force / vorticity_confinement. Tests: +4 fields (buoyancy rises, force direction, +confinement preserves curl, source plume), +1 integration (smoke from a heat source). README counts, NOTES, +tour. + +================================================================================ +Immersed boundary: solid OBSTACLES the flow goes around [+4 tests: 1760 -> 1764] +================================================================================ + +Closes the "solids as real obstacles" gap: until now the softbody could exchange MOMENTUM with the fluid +(scatter/drag), but a body did not BLOCK the flow. Now a solid mask diverts the fluid and smoke around it. + +BUILT (holographic_fields): + * disc_mask(shape, center, radius) -- a round solid obstacle (1 inside). + * enforce_solid(vx, vy, solid_mask, solid_vx, solid_vy, iters) -- the immersed-boundary step: inside the + mask, force fluid velocity to the solid's velocity (0 for a static obstacle), then re-project to + divergence-free so the displaced flow goes AROUND the solid; repeat a couple of times because each + projection slightly re-leaks velocity into the solid. + * fluid_step / smoke_step gain a `solid=` argument: enforce the obstacle after advection and forbid + density/temperature from entering it. + +MEASURED: a driven +x flow past a disc -- speed INSIDE the disc collapses to ~3% of ambient (the obstacle +blocks it); a rising smoke plume meets a disc and goes AROUND it (density inside the obstacle is exactly 0, +the rest of the smoke routes past). enforce_solid alone drops in-mask speed below 25% of the surrounding flow. + +KEPT NEGATIVES (honest): on a PERIODIC FFT grid this is an APPROXIMATE no-slip, not exact -- the global +projection re-leaks a little velocity into the solid each step (hence the iterate), and there is no true +boundary layer; the "flow accelerates at the shoulders" potential-flow signature is real but a full-ring +average is dominated by the stagnation/wake, so we assert the robust facts (blocked inside, smoke routes +around) and do not over-claim shoulder speed-up. Still the torus (a wake wraps); the obstacle is voxelized +(staircased at the disc edge). + +Wired: holographic_fields disc_mask / enforce_solid + `solid=` on fluid_step/smoke_step; UnifiedMind +faculties disc_mask / enforce_solid + `solid=` threaded through the fluid_step/smoke_step faculties. Tests: ++3 fields (enforce zeros velocity, obstacle blocks flow, smoke around obstacle), +1 integration. README +counts, NOTES, tour. + +================================================================================ +3-D fluid + smoke (the layer generalised from 2-D to 3-D) [+6 tests: 1764 -> 1770] +================================================================================ + +The fluid/smoke layer was 2-D; this generalises it to a 3-D periodic grid. The point worth stating: the bind +operator's circular convolution is DIMENSION-AGNOSTIC (it's an FFT on a torus of any rank), so the fluid +solver is too -- the 3-D operators are the 2-D ones with one more axis, via the n-D real FFT (rfftn/irfftn). +Added ALONGSIDE the 2-D functions (additive, the tested 2-D path untouched). + +BUILT (holographic_fields), grid shape (Nx, Ny, Nz), velocity (vx, vy, vz) on axes (0,1,2), 'up' = +y: + * _wavenumbers_3d / _trilinear_periodic -- 3-D spectral wavenumbers (same Nyquist-zeroing for first + derivatives) and a trilinear periodic sampler. + * diffuse_3d, divergence_3d, curl_3d (a vorticity VECTOR now), project_divergence_free_3d, advect_3d. + * fluid_step_3d, smoke_step_3d (buoyancy along +y; full 3-D vorticity confinement f = epsilon * N x omega). + +MEASURED: diffuse_3d conserves mass and smooths; PROJECTION drives 3-D divergence 13.81 -> 3.6e-15 (the +Nyquist care generalises exactly); advect_3d moves a blob v*dt; fluid_step_3d stays incompressible (~1e-15); +3-D smoke rises in +y (centroid 5.2 -> 14.3). + +GOTCHA LOGGED: NumPy 2.0 deprecates irfftn(s=...) WITHOUT axes -- it now needs axes=(0,1,2) explicitly (the +dedicated irfft2 does not). All 3-D irfftn calls pass axes; tests run clean under -W error::DeprecationWarning. + +KEPT NEGATIVES: 3-D advection/diffusion costs ~N^3 per field and the FFTs are 3-D -- much heavier than 2-D, so +grids stay modest; same periodic torus (no walls; the 3-D immersed-boundary mask was not added this pass -- +the obstacle work stayed 2-D); semi-Lagrangian advection still diffusive. + +Wired: holographic_fields _wavenumbers_3d / _trilinear_periodic / diffuse_3d / divergence_3d / curl_3d / +project_divergence_free_3d / advect_3d / fluid_step_3d / smoke_step_3d; UnifiedMind faculties fluid_step_3d / +smoke_step_3d / make_incompressible_3d / field_divergence_3d. Tests: +5 fields (diffuse, projection, advect, +fluid incompressible, smoke rises), +1 integration. README counts, NOTES, tour. + +================================================================================ +VSA-native tiling on FPE fields + the grid<->hypervector bridge [+7 tests: 1770 -> 1777] +================================================================================ + +This answers a pointed architectural question: (a) can the 3-D grid work improve tiling, and (b) is the recent +physics actually VSA-NATIVE or just numpy exposed as faculties? + +THE HONEST AUDIT. holographic_fields.py imports ONLY numpy -- the fluid solver's FFT *rhymes* with bind +(circular convolution on a torus) but the code never touches bind/bundle/cleanup and fields are numpy grids, +not hypervectors. So the physics was EXPOSED (callable faculties, deterministic, composable at the faculty +level) but NOT VSA-native. holographic_softbody.py is one step better (it reuses project_onto_constraints, the +shipped iterate-a-projection engine) but still on numpy position arrays. The right fix is NOT to run the FFT +fluid solve in hypervector space (that would be slow -- numpy grids ARE the efficient substrate); it is to +provide a BRIDGE so a result becomes a hypervector once, then composes in VSA. + +THE BRIDGE ALREADY HALF-EXISTED: FPE. holographic_fpe.VectorFunctionEncoder encodes a field as a hypervector +where a SHIFT IS A BIND (bind(f, encode(delta)) translates the whole field, exact to 1e-16), it is n-D (a +shift on any axis is a bind), and it is periodic. So: + +BUILT holographic_tiling.py (bind/bundle on FPE hypervectors -- fully VSA-native, nothing leaves VSA space): + * tile(enc, function, period, counts) -- domain repetition (Quilez's mod-tiling) as bundle-of-bind-shifts; + the result is a composable hypervector. n-D: 2-D AND 3-D for free, because FPE is n-D. (THIS is how the + 3-D grid work improves tiling: the same periodic-torus structure, now a 3-D motif tiled by 3-D binds.) + * tile_recursive(...) -- INCEPTION: tile the tiling L deep -> count^L copies per axis from L*prod(counts) + binds, in ONE fixed-size vector. Recursion + compression: 8x8=64 tiles from 12 binds, measured. + * fractal_bands(...) -- multi-scale (fBm) bundle: the motif at period p, p/2, p/4 ... summed (demoscene + fractal noise, in VSA). + * grid_to_function / function_to_grid -- the BRIDGE: a numpy field <-> an FPE hypervector (one encode per + significant cell -- the single crossing into VSA), so a fluid density / SDF slice can be tiled, bound, + bundled, stored like any VSA object. + +MEASURED: a tiled motif's copy reads EXACTLY equal to the original (shift-is-bind, 1e-9) with empty gaps +between cells (localized, gap ~ -0.01 at tuned bandwidth); 3-D tiling places a copy in each 3-D cell; +recursion puts the motif in the far corner of a 64-cell field built from 12 binds; the grid<->hypervector +round-trip correlates 0.99; and end-to-end a density blob is crossed into VSA and tiled 2x2 with a copy in +every cell. + +KEY TUNING / KEPT NEGATIVES: FPE bandwidth must SCALE WITH THE BOUNDS (bw ~16 over a range of 30, ~40 over 80) +to localize the motif -- too small and the kernel is so wide the gaps read higher than the tiles (logged with +the fix). Recursion's ceiling is VSA CAPACITY: count^L motifs share one fixed dim, so SNR falls as the tiling +grows (the far-corner read drops from ~0.34 for one tile to ~0.13 at 64 -- still recoverable at dim 4096, but +this is the cliff). The bridge costs one encode per significant cell (so threshold aggressively); the heavy +numerics stay on the grid by design. + +THE COMPOUNDING POINT. Because tiling now returns a hypervector, it composes with everything else: tile a +fluid puff, BIND it to a position role, BUNDLE several into a scene, store in the archive, recall by region -- +recursion, fractals, inception, compression, all from the one bind+bundle algebra. As above, so below. + +Wired: holographic_tiling.py (tile / tile_recursive / fractal_bands / grid_to_function / function_to_grid); +UnifiedMind faculties tile_field / tile_field_recursive / fractal_field / grid_to_hypervector / +hypervector_to_grid (beside the existing vector_function_encoder). Tests: +6 tiling, +1 integration. README +counts, NOTES, tour. + +================================================================================ +Seamless fractal volumes: the 3-D torus as a tiling SOURCE [+5 tests: -> 1782] +(plus an audit: the tiling layer + the recent physics are already VSA-composable) +================================================================================ + +THE QUESTION: can the 3-D grid improve our tiling? AUDIT FIRST (de-dup). holographic_tiling.py already does +exactly what the brief asks: domain repetition as bind+bundle on FPE field hypervectors ("only binds and a +sum, nothing leaves VSA space"), n-DIMENSIONAL so 3-D is free, tile_recursive = inception (count^L copies from +L*prod(counts) binds, one fixed-size vector), fractal_bands = fBm, and grid_to_function / function_to_grid = +the bridge that turns a NumPy field (a fluid density, an SDF slice) into a composable hypervector -- "simulate +on the grid, cross into VSA ONCE, then tile/bind/bundle/store." So tiling is already VSA-native, already 3-D, +already the physics bridge. The recent physics additions are already exposed (UnifiedMind faculties) and the +heavy work is in-array FFT (the boundary is crossed once per step, not per voxel) -- consistent with the +thesis. Nothing there needed rebuilding. + +THE GENUINE GAP the 3-D grid fills: nothing SYNTHESISED a seamless field to tile. The FFT torus is the natural +seamless source (periodic by construction). Added (holographic_fields): + * spectral_field(shape, beta, seed) -- a SEAMLESS FRACTAL volume (2-D or 3-D) synthesised in Fourier: + amplitude |k|^(-beta/2) (1/f^beta -> fractal) with random phases, inverse real FFT -> zero-mean unit-std, + PERIODIC by construction so it tiles with no seam. The demoscene 'rich volume from a tiny seed': the whole + volume is reproducible from (shape, beta, seed) -- that compression IS the point. + * seam_continuity(field) -- the wrap-jump / interior-jump ratio (~1 = seamless). + +MEASURED: spectral seam ratio 1.07 vs a non-periodic ramp 31 (seamless); beta controls roughness (0.91 at +beta=0.5 -> 0.38 at beta=3.0); a 4096-voxel volume is byte-identical from 3 numbers (compression). THE +COMPOUNDING, measured end to end: (1) a localized motif from the volume crosses into VSA once and tiles 3-D as +binds+sum -- 8 copies in one hypervector, the far copy reading identically (0.33 == 0.33); (2) a FRACTAL +initial temperature makes the 3-D smoke plume markedly MORE vortical (total vorticity 8006 -> 13542). Fractal +source x VSA tiling x the FFT fluid solver -- three layers composing, each crossing into VSA once. + +KEPT NEGATIVES: grid_to_function bundles one encode PER significant cell, so the composable unit is a LOCALIZED +motif (a puff, a surface), not a dense noise volume -- tiling count^L localized motifs is fine, but encoding a +full dense volume as one hypervector blows VSA capacity (the kept negative already in the tiling module). The +seamless source is a GRID primitive (the seam guarantee is the torus); the hypervector tiling inherits +seamlessness only for motifs that fit within a period. + +Wired: holographic_fields spectral_field / seam_continuity; UnifiedMind faculties spectral_field / +seam_continuity (tiling faculties already existed). Tests: +3 fields (seamless, roughness, deterministic), +2 +integration (fractal volume tiled in VSA; fractal initial condition enriches 3-D smoke). README synced to the +actual collected count (had drifted), NOTES, tour. + +================================================================================ +fractal_volume: fractal source -> inception -> one hypervector, in ONE call [+4 tests: 1782 -> 1786] +================================================================================ + +The single composable entry point for the whole pipeline. holographic_tiling.fractal_volume(enc, period, +counts, levels, beta, seed) does, in one call: synthesise a LOCALIZED fractal grain (spectral_field under a +Gaussian envelope, a POSITIVE bump modulated by 1/f^beta detail -- localized so it respects the capacity +ceiling) -> cross into VSA ONCE (grid_to_function) -> tile_recursive it `levels` deep (count^levels self- +similar copies per axis from L*prod(counts) binds). Returns ONE fixed-size hypervector. 2-D and 3-D. + +WHY POSITIVE GRAIN (a fix worth recording): a raw spectral_field grain is ZERO-MEAN, so FPE's value-weighted +query finds no localized peak (reads ~0). The motif must be a positive localized bump (envelope * (1 + 0.6 * +fractal)) for the tiled copies to be detectable. Zero-mean textures don't survive as FPE motifs -- localized +positive features do. + +MEASURED: 2-D, counts=2 levels=2 -> 4 self-similar copies per axis, all reading ~0.24-0.25 consistently, in +one 8192-vector; 3-D works the same. Composable downstream: bound to a random role and unbound, the volume +recovers at cosine 0.707 -- moderate (binding a STRUCTURED vector is noisier than a random one, an honest HRR +fact) but clearly not noise (an unrelated vector reads ~0). KEPT NEGATIVE (capacity): more copies share one +fixed dim, so the per-copy read falls -- 0.24 at 4 copies/axis -> 0.12 at 9. count^levels structure is free in +binds but costs SNR; the localized-motif rule is what keeps it usable. + +THE THREAD THIS CLOSES (the compounding Moose asked for): spectral_field (seamless fractal SOURCE on the 3-D +torus) -> grid_to_function (cross into VSA once) -> tile_recursive (inception) -> one hypervector, now behind +ONE faculty. Recursion, fractals, inception, compression, demoscene magic -- one call, composable as any VSA +object (bind/bundle/store), and the whole self-similar volume specified by (beta, seed) + a handful of binds. + +Wired: holographic_tiling.fractal_volume; UnifiedMind faculty fractal_volume. Tests: +3 tiling (recursive +tiling, capacity falloff, 3-D), +1 integration (one-call + composability). README synced (1782 -> 1786 actual), +NOTES, tour. + +================================================================================ +fractal_volume, generalized: inception over ANY VSA object [+5 tests: 1786 -> 1791] +================================================================================ + +fractal_volume's seed is no longer just a synthesized fractal grain -- it's ANY VSA object. New (additive, +backward-compatible) kwargs on holographic_tiling.fractal_volume / the UnifiedMind faculty: + * motif= -- used directly: a smoke puff (a density field crossed into VSA), an SDF surface, a + stored archive image, or the OUTPUT OF ANOTHER fractal_volume. tile_recursive replicates it count^levels + deep. The default-grain path is unchanged (motif=None, motif_grid=None). + * motif_grid=, motif_coords=... -- a NumPy field crossed into VSA ONCE (grid_to_function), then + tiled. physics -> inception: simulate a feature on a grid, tile it self-similarly. + +INCEPTION OVER THE ENGINE ITSELF: a fractal_volume's output IS a hypervector, so feed it back in as the motif +of another fractal_volume -> copies-of-copies, all binds, one fixed-size vector. The operator now closes over +its own output -- self-similar structure of self-similar structure. + +HONEST FRAMING (kept explicit in the docstring + a test): ANY hypervector tiles into a VALID composable +hypervector (it's all binds and a sum -- bind it to a role, bundle it, store it, clean it up). But the SPATIAL +read-back (enc.query at a copy) is meaningful only for FPE-FUNCTION motifs (the grain, a grid_to_function +field, another fractal_volume's output). An arbitrary non-FPE plate (e.g. a random concept vector) still tiles +into a valid bundle -- finite, non-degenerate, round-trips through the algebra at cosine ~0.7 (structured-vec +recovery, not the ~1.0 of a random vector, and clearly above an unrelated vector) -- just not a spatially +queryable one. We assert composability + identifiability there, NOT a spatial read. + +MEASURED: default grain, motif=FPE-point, and motif_grid=puff all give 4 self-similar copies reading +~0.24-0.25; inception (fv of fv) keeps copies-of-copies (>=3/4 present); an arbitrary concept vector tiles into +a composable hypervector (round-trip cosine ~0.7, >> unrelated). Backward compat: the default path is +byte-for-byte the prior behaviour (the 3 original fractal_volume tests still pass). + +Wired: holographic_tiling.fractal_volume (motif / motif_grid / motif_coords); UnifiedMind faculty updated to +pass them through. Tests: +4 tiling (hypervector seed, physics-grid seed, inception-over-output, arbitrary-VSA- +object composability), +1 integration (inception + motif_grid through UnifiedMind). README synced (1786 -> +1791), NOTES, tour. + +================================================================================ +inception(depth): one-parameter recursion depth + an honest capacity ceiling [+3 tiling, +1 integ: 1791 -> 1795] +================================================================================ + +`inception(enc, period, counts, depth, motif=None, ...)` exposes fractal_volume's recursive tiling as a single +DEPTH knob and returns (volume, profile). + +DE-DUP, KEPT HONEST: I probed first and confirmed nesting fractal_volume on its own output is BIT-FOR-BIT +identical to fractal_volume(levels=depth) (max|A-B| = 0.0). tile_recursive already feeds each level's output +back in at a period grown by counts, so a plain inception(depth) would just rename the existing `levels` +parameter -- the canonical failure mode (build something already shipped). So inception does NOT ship new +tiling math. The volume IS fractal_volume(levels=depth), documented as such and asserted bit-identical in a +test. + +THE GENUINELY-NEW PART is the `profile`: at each depth 1..depth it reports copies_per_axis, mean per-copy read +(enc.query at the tiled instances), and the role-binding round-trip recovery -- so the capacity ceiling of +nesting is a MEASURED table, not a footnote. MEASURED (counts=2): per-copy read falls monotonically +0.787 (depth1, 2 copies) -> 0.510 (depth2, 4) -> 0.283 (depth3, 8) as counts**depth instances share one fixed +dim, while whole-vector recovery stays ~0.71 throughout. Two distinct notions of fidelity: the per-copy READ +(spatial SNR, degrades with depth) and the whole-vector RECOVERY (the bundle still composes through binding, +roughly constant). One parameter trades richness for fidelity, and now you can see the trade. + +Wired: holographic_tiling.inception + UnifiedMind faculty. Tests: +3 tiling (composable volume + profile, +read-falls-with-depth, volume==fractal_volume(levels) exactly), +1 integration (profile through UnifiedMind). + +================================================================================ +The 3-D physics gaps: obstacle + particle<->field coupling + cloth self-collision [+4 fields, +3 softbody, +3 integ: 1795 -> 1804] +================================================================================ + +Three gaps in the field/softbody layer filled, each the 3-D lift of a 2-D operator that already shipped, kept +VSA-native (FFT = bind) and composable (faculties + returned objects other VSA programs drive). + +(B) 3-D IMMERSED BOUNDARY. sphere_mask((Nx,Ny,Nz), center, radius) (the ball, disc_mask lifted) + +enforce_solid_3d(vx,vy,vz, mask, ...) (force flow to the solid velocity inside the mask, re-project +divergence-free so the flow diverts AROUND it). Threaded `solid=` through fluid_step_3d and smoke_step_3d. +MEASURED: flow ~1% of ambient inside the ball, density 0 in the solid (routes around). KEPT NEGATIVE: on the +periodic FFT grid this is an approximate, not exact, no-slip (same caveat as 2-D enforce_solid). + +(C) PARTICLE<->3-D-FIELD COUPLING. sample_field_3d (trilinear read at (N,3)) + scatter_to_field_3d (its EXACT +adjoint: == , the VSA-native "bind and its transpose") + drag_force_3d +(k*(v_fluid - v_node), the fluid->body half). A SoftBody now couples to fluid_step_3d EXACTLY as it does to the +2-D solver: pass external_force=drag_force_3d(...). MEASURED: the adjoint identity holds to machine precision; a +soft strip in a uniform 3-D flow drifts downstream and stays intact (residual 0). + +(D) CLOTH SELF-COLLISION via a REUSABLE CULL PRIMITIVE. spatial_hash_pairs(positions, radius) buckets points +into cells of size `radius` and tests only the 3^D neighbour block -> O(N + pairs) expected, the "cull, don't +batch" lesson again (matches brute force EXACTLY, 82/82 pairs on a test). SoftBody.add_self_collision(radius) +(opt-in, default off, excludes directly-bonded pairs) + _solve_collisions() pushes non-bonded penetrating pairs +apart to the radius -- another iterate-a-projection in the solver sweep. + BUG KEPT ON RECORD: the separation push leaked into the PBD velocity readback (v=(x-x_prev)/h), so two nodes + flew apart ballistically (1.0 -> 7.3 over 10 steps). FIX: treat collision as a POSITIONAL contact resolve -- + snapshot x before the push, subtract the collision displacement from the velocity update, so a contact + separates nodes WITHOUT injecting coasting momentum. After the fix: two overlapping nodes REST at exactly the + radius (v=0); 5 overlapping nodes spread from min-gap 0.10 (off) to 1.00 (on); a bonded pair stays at its + rest length (excluded). (Also hit and removed a transient DUPLICATE 3b collision block from two edits -- the + shadowing collision ran twice and its first push wasn't velocity-corrected. Watch for double-applied edits.) + +REUSE OPPORTUNITY (noted, not yet adopted): spatial_hash_pairs is a general close-pair finder -- a candidate to +speed up non-local-means patch matching and particle interaction, the same culling win the mesh-distance and +sculpting work kept re-learning. Exposed as a faculty so other VSA programs can use it directly. + +Wired: fields (spatial_hash_pairs, sphere_mask, enforce_solid_3d, sample_field_3d, scatter_to_field_3d, +drag_force_3d, solid= on the two 3-D steps), softbody (add_self_collision, _solve_collisions), UnifiedMind +faculties for all six fields functions + solid= on the 3-D step faculties. Tests: +4 fields, +3 softbody, +3 +integration (inception profile, 3-D fluid obstacle + softbody coupling, self-collision on a mind-built cloth). + +================================================================================ +De-dup fix: the encode_record name collision (one name, one faculty) [0 tests; unblocks a failing test] +================================================================================ + +A full-suite run surfaced a latent bug: UnifiedMind had TWO faculties both named `encode_record` -- an older +`encode_record(self, fields)` (a {field: value} record, paired with decode_record) and a later +`encode_record(self, keys, values)` (a batched bundle_bind of parallel key/value arrays). In Python the later +class-body definition WINS, so the batched two-arg version silently shadowed the record encoder, and +`m.encode_record(rec)` (the one-arg record API, with a test + a decode partner) failed with a TypeError. Not +caused by recent work -- it predated it and was only hidden because in-session runs stay focused. + +FIX: the `(fields)` version keeps the name `encode_record` (it has the stronger claim -- a decode_record +partner and record semantics); the batched primitive was renamed to `encode_pairs(keys, values)` (bundle of +bind(key_i, value_i) in one FFT). Updated its one caller (test_holographic_primitives). Both now coexist: +encode_record round-trips a record, encode_pairs vectorises a role/filler encode. The lesson is the project's +own: probe live code for name collisions; two implementations under one name is a silo even when both work. + +================================================================================ +spatial_hash_pairs put to work: short-range particle repulsion (cull, don't batch on the particle layer) [+2 fields, +1 integ: 1804 -> 1807] +================================================================================ + +Applied the new spatial_hash_pairs cull primitive at a SECOND site. First I probed the two candidates honestly: + * NLM denoise -- NOT a fit (kept-negative scoping). Its neighbour search is in high-dim PATCH-FEATURE space + (cosine similarity), already culled by HoloForest random-projection trees. A uniform grid hash is the wrong + tool in high dimensions (the 3^D neighbour block explodes; grid distance != cosine). The forest stays. + * Particle layer -- a REAL GAP: the particle system had no particle-particle interaction at all (only point + attractors and field drag). The classic n-body short-range force is exactly what a spatial hash is for. + +So `pairwise_repulsion(positions, radius, strength)` (holographic_fields + faculty): for every pair within +`radius` (found by spatial_hash_pairs), a soft-sphere push that falls linearly to zero at the radius, summed +per particle, returned as an (N, D) force array -- composable like attractor_force / drag_force, fed to +ParticleSystem.step(force=...). Any dimension. + +MEASURED: culled force == the O(N^2) all-pairs brute sum EXACTLY (the hash changes cost, not answer). The cull +win grows with N (fixed density), even against a half-vectorised brute: 1.6x at N=500, 2.5x at N=2000, 3.5x at +N=5000 -- the O(N + pairs) vs O(N^2) scaling. (Honest: the per-pair push is a Python loop, so the win is from +CULLING work, not vectorising it; at small N the bucketing overhead makes it only modestly faster. Same lesson +as the mesh-distance/sculpting work: a spatial index that culls beats batching a dense reduction.) + +Wired: holographic_fields.pairwise_repulsion + UnifiedMind faculty. Tests: +2 fields (matches-brute-exactly, +disperses-a-clump), +1 integration (repulsion + faculty-equals-standalone through UnifiedMind). + +================================================================================ +Vectorising the per-pair Python loops: spatial_hash_pairs + the scatter accumulators [+1 fields: 1807 -> 1808] +================================================================================ + +Profiling pairwise_repulsion exposed the real bottleneck: the per-pair force loop was only ~22%; the OLD +dict-based spatial_hash_pairs was ~78% (209 ms at N=5000). Fixed both, keeping everything in array-land (the +performance thesis: don't cross the Python<->VSA boundary per pair). + +spatial_hash_pairs, NOW VECTORISED (sort + searchsorted cell list). Linear-index each point's cell, SORT by +that key, and for each of the (3^D+1)/2 canonical offsets use searchsorted to find -- for every point at once -- +the contiguous block of points in the target neighbour cell, expand those ragged blocks into candidate pairs +(the standard repeat/cumsum ragged-range trick, no Python per-range loop), and filter by true distance. The +ONLY Python loop left is over the 3^D offsets (a fixed constant, not per-point/per-pair). Returns pairs sorted +by (i,j) for determinism. MEASURED: matches brute force EXACTLY in 2-D and 3-D; ~18x faster (N=5000: 209 ms -> +11.5 ms; N=10000: 77 ms). The aliasing guard: pad the cell grid by 1 on each side so cell+/-1 never wraps to a +different cell's linear index. + +pairwise_repulsion force accumulation: the per-pair Python loop became a SCATTER -- compute all pair forces as +arrays, then np.add.at(F, i, f) / np.add.at(F, j, -f). That np.add.at IS the same adjoint/scatter as +scatter_to_field (bind and its transpose). Order-independent sum, so still == the O(N^2) brute EXACTLY. End to +end pairwise_repulsion is ~19x faster (270 ms -> 14 ms at N=5000). + +SoftBody._solve_collisions: same treatment -- vectorised to a Jacobi scatter. Find pairs (hash), drop bonded +pairs by a VECTORISED key-membership test (min*N+max keys vs a precomputed self._bonded_keys, via np.isin), +compute all corrections as arrays, scatter with np.add.at. KEPT-NEGATIVE / honest behaviour change: this is +JACOBI (all corrections from one state, summed) rather than the old sequential GAUSS-SEIDEL loop. A node in +MULTIPLE simultaneous collisions now gets the summed push, so a tight clump spreads a little FURTHER (5 +overlapping nodes settle at min-gap ~1.85 vs the old ~1.00 at radius 1.0). All the load-bearing properties hold: +two nodes rest at exactly the radius with v=0, bonded pairs stay excluded, the clump disperses to >= radius. +Jacobi is order-independent and deterministic -- which the bind_batch tie-break lesson rewards (no +order-dependent knife-edge). The default single pass is unchanged in cost shape; add iterations later if tight +packings need it. + +LESSON: "cull, don't batch" has a twin -- once you've culled to the close pairs, ACCUMULATE them as a scatter +(np.add.at), not a Python loop. The scatter is the adjoint of the gather/sample, so the whole short-range-force +/ collision step is now one gather (the hash) + arithmetic + one scatter -- all VSA-native array ops. + +Tests: +1 fields (vectorized hash determinism/sorted/2-D+3-D contract); existing matches-brute and +collision-behaviour tests still green (the spread test asserts >0.9, Jacobi's 1.85 passes). + +================================================================================ +writing_vsa_programs.md updated: new program capabilities documented +================================================================================ + +The VSA program guide (the HoloMachine DSL) was missing FOUR live opcodes and the recent faculty additions. +Added: (1) STORE r / RECALL r (the 8-slot register file R0..R7 -- exact, no-crosstalk side storage beside ACC, +ISA-4) and PUSH / POP (the permute-stack for save-and-restore / nesting, ISA-5) to the instruction-set table, +the operand-codebook note (STORE/RECALL clean against register names), and a new "Registers and a stack" +section with runnable examples (both verified cosine 1.0). (2) A "faculty catalogue a program can compose" +section listing the recent additions grouped by domain (encode_pairs rename, fractal_volume/inception, the 3-D +fields + immersed boundary, particle<->3-D-field coupling, spatial_hash_pairs + pairwise_repulsion, softbody +self-collision), with the two ways a program reaches a faculty: as an acc->acc APPLY handler, or as host +orchestration of the grid/particle layer. Every code example in the guide was run. + +## Stable mesh projection, topology guarantees, stable UVs, mesh→physics bridge, and blue-noise sampling (+9 tests, 1808→1817) + +The goal: a 3-D modeling app on top of this stack needs projected meshes whose faces/edges/verts don't move on +their own, are topologically clean, and align with UVs predictably — plus the projected mesh should be able to +use the physics layer we built. + +**Root cause of "verts moved elsewhere after an edit" (diagnosed + measured).** `surface_mesh` extracts via +`marching_tetrahedra_vec`, which is inherently 2-manifold (marching *tetrahedra* has none of marching cubes' +ambiguous non-manifold cases). Vertices are deduped by a packed *edge key* (the two grid corners the vertex +interpolates between), but the final vertex *array* is built from `np.unique(keys)`, which **sorts** the keys +and hands out sequential indices. So positions are deterministic, but a local edit that adds/removes one +surface crossing shifts the sorted order and **renumbers every vertex after it**. Measured on a res-40 sphere +with a local +z dimple: 7613 shared vertex identities, far-from-edit positions bit-identical, but **4494 of +those same vertices got a different array index** — the "phantom movement" a frontend tracking by index sees. +By *key*, zero move. + +**Fix — stable vertex identity.** `marching_tetrahedra_vec(..., return_keys=True)` now also returns the +canonical per-vertex edge key. A frontend tracks vertices by KEY (persistent identity), not array index. KEPT +NEGATIVE: keys are tied to the grid, so they're stable across *edits* at a fixed (resolution, bounds), **not** +across resolution changes — a different resolution is a different mesh by definition. + +**Topology guarantee — `Mesh.validate_topology()`.** Full report: manifold edges (every undirected edge ≤2 +faces), manifold *vertices* (the bowtie case the edge test MISSES — two cones meeting at a point pass the edge +check; caught here by union-find on each vertex's 1-ring link graph: a clean fan is one component), watertight, +degenerate faces, euler/genus, and one `ok` flag. Verified: clean sphere ok/watertight/euler2/genus0; a +constructed bowtie → `manifold_edges True` (test passes) but `manifold_vertices False, non_manifold_verts [0]`; +degenerate face caught. + +**Clean-extraction guarantee — `surface_mesh_stable`.** The one marching-tet non-manifold cause is a field +sample landing *exactly* on a grid corner (vertex on the corner → shared by many tets → bowtie). The faculty +nudges exact-corner samples a deterministic epsilon off the level so the crossing lands on the edge interior, +guaranteeing 2-manifold output, and returns `{mesh, keys, topology}` — the modeling-app entry point. + +**Stable UVs — `stable_uv` / `mesh_stable_uv`.** The global unwraps (isomap/planar-PCA/spectral) solve an +MDS/eigenmap over the whole mesh, so a local edit shifts every UV and the solution carries a sign/rotation +ambiguity (the chart flips on re-run) — the UV version of the index problem. `stable_uv` makes UVs a +deterministic function of WORLD POSITION (triplanar picks each vertex's plane by its normal so curves don't +fold), normalized by the FIXED field bounds so the scale is edit-invariant. Measured: 5165/5165 far-from-edit +verts keep identical UVs across a local edit. KEPT NEGATIVE: this is stable *texturing*, not a seam-cut chart; +for a faithful low-distortion unwrap use `uv_unwrap` and accept that it re-solves. + +**Mesh→physics bridge — `SoftBody.from_mesh` / `mesh_to_softbody`.** No bridge existed (SoftBody only had +parametric cloth/rope/soft_box builders). Now a projected mesh's verts→particles and edges→distance +constraints, so it can be driven by gravity, fluid drag (`drag_force_3d`), self-collision, the constraint +solver — sculpt → surface_mesh → from_mesh → simulate. Verified: a projected sphere → 7754 particles, 23256 +constraints, rides a 3-D fluid. KEPT NEGATIVE: a surface mesh is a shell, so it behaves like cloth, not a +filled solid — add bending / soft_box semantics for volume resistance. + +**Blue-noise sampling — `holographic_sampling.poisson_disk_sample` / `blue_noise_sample`.** The carry-over from +the omnipoint thought experiment: the exclusion principle, done right. Last session's naive repulsion-relaxation +under-converged (a kept negative). Bridson dart-throwing (accept a candidate only if no point is within radius, +checked against a background grid — "cull, don't batch") delivers genuine blue noise: hard min-distance +guarantee AND the spectral signature (low-freq power ratio 0.18 vs white, ring near the spacing). Measured +payoff on a fixed-budget splat fit: blue-noise centers 22.64 dB vs random 19.35 dB (+3.3 dB), within 0.4 dB of +adaptive matching pursuit (23.05 dB). HONEST SCOPE: `splat_fit`'s matching pursuit is already adaptive +(data-driven peak placement) and doesn't need blue-noise placement; blue noise is the right tool for +NON-adaptive placement — initialization, particle/stipple, Monte Carlo — where it nearly matches adaptive while +being data-independent. This validates the *algorithm*, not the cosmology. + +## Face-type control, dynamics→mesh export, PBR materials in standard formats, and 2-D splat export (+10 tests, 1817→1827) + +Goal: a 3-D modeling app on this stack can choose the face standard it projects out, export any dynamics state +as a mesh, export 2-D and 3-D splats, and import/export materials to the formats the ecosystem actually uses -- +keeping everything on the engine's own field/VSA bridges. + +**Format grounding (searched).** The ecosystem has converged on glTF 2.0's **metallic-roughness** PBR +(baseColorFactor, metallicFactor, roughnessFactor, emissiveFactor) -- ISO/IEC 12113:2022, ~1:1 with MaterialX's +glTF-PBR node and USD's UsdPreviewSurface, identical to Blender/Unreal/Unity's Principled BSDF. That is the +canonical factor model adopted here. + +**Face-type control (`holographic_meshpoly.py`).** Marching tetrahedra emits triangles; quads/ngons are a +deterministic merge ON TOP that leaves vertices (and their stable keys) untouched. `triangles_to_quads` greedily +pairs the most-coplanar adjacent triangle pairs into convex quads (quad-dominant, Blender "Tris to Quads"), +leftovers stay triangles; `merge_coplanar` region-grows connected coplanar faces and emits each flat region whose +boundary is a clean loop as one n-gon (a flat wall -> one face). Wired as `surface_mesh_stable(..., face_type=)` +and standalone `mesh_face_type`/`mesh_face_counts`. Measured: a marched sphere (9648 tris) -> 4369 quads + 910 +tris, still watertight/manifold; a marched box (6624 tris) -> 48 coplanar faces, watertight. KEPT HONEST: the +face GROUPING is not edit-stable (a flat region's ngon boundary moves when edited) -- faces are a derived view, +vertices are the stable identity; the bowtie vertex check only runs on all-triangle meshes (edge/watertight +checks still run on polygons). + +**Dynamics→mesh export (`dynamics_to_mesh` faculty).** Everything surfaces through the engine's own field/mesh +bridge: a point cloud (particles / LIQUID front) -> a `metaball_field` (sum of Gaussians) marched at a level; a +density grid (SMOKE) -> marched directly at a level; a SoftBody/RigidBody -> its current positions + faces. +Required fix: `SoftBody.from_mesh`/`RigidBody.from_mesh` now RETAIN the source faces (they were dropped), and both +get `to_mesh()` -- so a DEFORMED soft body or a MOVED rigid body re-exports as geometry (sculpt -> mesh -> +simulate -> to_mesh -> export). Verified across all four sources; `face_type` applies to the result. + +**PBR materials in standard formats (`holographic_materialio.py`).** A `PBRMaterial` (the glTF factor model) is +the single representation every path maps through: `to_gltf_dict`/`from_gltf_dict` (the writer now embeds the +material's factors, not the old hard-coded default), `to_mtl`/`materials_from_mtl` (the OBJ companion, modern +Pr/Pm/Ke keywords + legacy Kd/d/Ns so it round-trips with PBR tools and opens in old ones). Wired as +`pbr_material`, `mesh_to_gltf(..., material=)`, `material_to_mtl`, `materials_from_mtl`. MTL and glTF round-trip +EXACTLY. VSA-NATIVE carrier: `to_vsa_record(scalar_encoder)` scalar-encodes each factor, binds it to its channel +role, and bundles -- so a material transmits/composes/BLENDS as ONE hypervector (like a splat scene or typed +record), recovered by unbind+decode. KEPT HONEST: the VSA record is crosstalk-limited (9 channels in one +vector) -- ~0.024 factor error at dim 8192, ~0.06 at 2048; for lossless materials use the exact MTL/glTF path, +the VSA record is for engine-side compose/blend. Factor-level only for now (no image TEXTURE maps yet -- the +next step, flagged not faked). + +**2-D + 3-D splat export.** 3-D splat PLY (3DGS standard) already existed; `splats_2d_to_records` lifts +`splat_fit`'s 2-D image splats ((cy,cx,amp,sigma)) onto the z-plane (center=(cx,cy,z), isotropic L=1/sigma) so +2-D and 3-D splats export through one path (`export_splats_2d` faculty). Verified: a 2-D fit exports and reads +back from the standard .ply. + +## A CPU rendering subsystem: camera, lights, mesh rasteriser, volumetric renderer, tile-delta streaming (+5 tests, 1827→1832) + +The toolkit could PROJECT geometry and emit a GPU shader but had no camera, lights, or way to produce a +rasterised IMAGE itself. `holographic_render.py` adds that, built to fit the engine's representations. + +**Camera + Lights.** `Camera` (eye/target/up/fov) gives view+projection matrices and per-pixel world-space rays; +`Light` is directional / point / ambient. The missing viewpoint primitives. + +**Mesh rasteriser (`rasterize_mesh`).** A z-buffered, flat-Lambert CPU rasteriser: world->clip->screen, per-face +frustum + back-face CULL ("cull, don't batch"), then each visible triangle's pixels filled vectorised over its +screen bbox via barycentric edge functions; shaded by the lights and a base colour (a PBRMaterial's base_color +works). Verified: a lit sphere shows a real bright->dark gradient with background where empty. + +**Volumetric renderer (`volume_render`) -- the VSA-native part.** Smoke, fire, water, and surfaced particles are +all density (and emission) FIELDS, so rendering one is marching camera rays through the field and accumulating +the volume-rendering integral (transmittance * emission, with absorption) -- VECTORISED over all pixels (one +field-sample call per step), with a ray/box slab test so only the volume is marched. mode='smoke' (lit grey +absorption), 'fire' (a blackbody ramp on density -> emissive glow), 'density' (raw). The field IS the volume -- +this is the field-native render the engine was set up for. Verified: smoke has an opaque core / transparent +edges (alpha 0..1), fire glows red. + +**PNG output (`save_png`).** A minimal pure-stdlib encoder (zlib + struct), so the module carries no image-library +dependency. `save_render` faculty. Sample frames rendered (sphere/smoke/fire) and eyeballed correct. + +**Tile-delta streaming (`frame_delta_tiles`).** The pixel-streaming primitive: split two frames into tiles and +return only the CHANGED tiles -- the rendering analogue of the engine's O(change) delta protocol. Measured: a +LOCAL edit dirties 10 of 64 tiles (16%), so a viewport pushes ~16% of the pixels, not the whole frame. + +**PERFORMANCE, ON THE RECORD (the honest answer to "Houdini/Maya parity").** This is a correctness-first CPU +renderer. Measured: rasterising a 21k-face sphere at 384x384 is ~1.7 s; the volumetric blob at 256x256x96 is +~1.7 s. That is NOT realtime and does NOT match a compiled, GPU, multithreaded DCC core (Houdini/Maya/C4D use +C++/GPU/OpenVDB; pure NumPy cannot match raw raster/sim throughput -- the mesh kernel already documents this +bound). What the VSA-native design DOES buy for heavy scenes is real but DIFFERENT: O(edit) holographic +complement (edit cost independent of model size), field-native LOD (coarsen the source, re-project), sparse +fields (O(brush) sculpting), the O(change) delta/patch protocol, and now tile-delta rendering -- i.e. the engine +is the authoring BRAIN that makes edits and streams DELTAS cheaply, while the GPU stays the MUSCLE for the heavy +interactive viewport. The CPU renderer here is for offline frames, previews, and headless/air-gapped rendering, +not for driving a million-poly viewport at 60fps. Kept loud so the claim is not oversold. + +## Porting the render Python-loop to a vectorized scatter, and V-Ray raymarching optimizations (+2 tests, 1832→1834) + +The ask: borrow more V-Ray ideas to optimize raytracing, and port Python-loop work to "VSA-native" (vectorized +array ops) to beat the NumPy bottleneck. The honest framing first: VSA ops ARE NumPy (bind = FFT, bundle = sum); +"VSA-native" does not bypass NumPy or reach GPU speed. What it genuinely buys is (a) replacing a Python +per-element LOOP with one vectorized array op (a scatter = a bundle), and (b) replacing an O(n) SEARCH with +content-addressable recall / caching. Both were demonstrated. + +**Existing V-Ray-analogous tech (the user was right -- it's under other names):** HoloForest (random-projection +tree ensemble) = a BVH/kd-tree for sublinear spatial recall; `adaptive_anchors`/`reconstruct_from_anchors` = an +irradiance-cache / light-cache (place samples where the signal varies, interpolate elsewhere); `manifold_denoise` +/`pnp_restore`/`nlm_denoise` = the V-Ray denoiser (render fewer samples, denoise). No CPU ray-triangle tracer +exists; the renderer rasterises and the volumetric path ray-marches. + +**Vectorized rasteriser (the headline win).** The per-triangle Python loop was the bottleneck. Ported to a single +vectorized fragment SCATTER -- cull to visible faces, expand each face's screen bbox into a flat fragment array +with repeat/cumsum (the ragged-expand from spatial_hash_pairs), compute every fragment's barycentric at once, and +resolve the z-buffer with ONE lexsort (sort fragments by pixel then depth, take the nearest per pixel). MEASURED: +7.8x faster at 13k faces, **15x at 30k faces** (the speedup GROWS with face count -- it is the Python-loop +overhead being removed), image bit-identical to the loop (`vectorized=False` kept as the reference). This is the +concrete proof of the user's point: same NumPy underneath, one batched scatter instead of N small loop bodies. +Still ~120 ms/frame (~8 fps) at 30k faces -- a real 15x, but not GPU-realtime; the GPU gap is hardware. + +**V-Ray volumetric optimizations (empty-space skipping + early ray termination).** Both default-on, +result-preserving (max pixel diff ~0.001): + * EMPTY-SPACE SKIPPING -- sample the field once on a coarse occ_res^3 macro grid (dilated by one cell); during + marching, only sample the fine field for rays whose macro-cell is occupied. "Cull, don't batch" for volumes. + * EARLY RAY TERMINATION -- drop rays whose transmittance < term_eps (opaque); no further samples. + MEASURED, with the kept negative: field EVALUATIONS drop 3x (sparse wisp, empty-skip) to 7x (dense blob, + early-term), but WALL-CLOCK only 1.1-1.5x -- because for CHEAP analytic fields the per-step vectorized overhead + (occupancy lookup, masking) dominates, not the field eval. The wall-clock win SCALES WITH FIELD-EVAL COST: an + expensive field (a large metaball sum, an FPE query, a learned field) would realise the full 3-7x; a couple of + exp()s does not. Kept loud. + +**The bottom line on "VSA-native beats the NumPy bottleneck."** Partly true, precisely: porting a Python loop to a +vectorized scatter is a real 8-15x (the rasteriser), and culling work (empty-space skip, BVH recall, irradiance +cache) is real. It is NOT a path to GPU-class realtime in pure NumPy -- the brain renders/optimises deltas, the +GPU stays the muscle for the heavy viewport. + +## Animation / deformation over time, frame caching, and the last classic mesh tools (+15 tests, 1834→1849) + +The ask: animate and deform meshes, particle clouds, and volumetric data over time; cache frames (the "L1-L4 / +RAM" idea); have the usual mesh-editing toolbox; keep it VSA-native and avoid Python loops. Probe-first found +most of the mesh toolbox already shipped (extrude/inset/bevel/bridge/loop_cut/flip/split/collapse/laplacian_ +smooth/loop_subdivide/qem+cluster decimate) and the blendshape primitive (blend_pose), morph_video, and +Propagator.rollout -- but no unified deform-over-time layer, no frame cache (holographic_cache.py is Ward's +IRRADIANCE gradient cache, not a frame cache), and three missing classics: mirror, weld, solidify. + +**Vectorized deformers (holographic_deform.py).** taper / twist / bend (Barr arc) / lattice_deform (trilinear +FFD, 8 gathers) all operate on any (N,3) array -- so a mesh's vertices and a particle cloud run the SAME path, +one array op each, no Python per-point loop. blendshapes(base, targets, weights) = base + weights @ deltas: a +WEIGHTED BUNDLE, the engine's superposition primitive applied to geometry, so animating the weights over time IS +the blendshape animation. HONEST LINE KEPT: the shape math (sin/cos of a bend) is plain NumPy, not a hypervector +trick; what is genuinely VSA-shaped is the blend (a bundle) and the rigid case (a bind, already in +HolographicField.translate). Verified: lattice identity + translation exact; blendshape endpoints exact; twist +invertible; bend curves a straight bar symmetrically. + +**Timeline + tiered delta FrameCache (holographic_anim.py).** Timeline keys values and samples a lerp at any t +(vectorised over a vector of times). FrameCache stores each frame as a sparse DELTA vs a base (O(change) memory +-- the engine's patch protocol on the TIME axis), reconstructs exactly, and keeps the `hot` most-recent frames +full in RAM for instant scrubbing. ON "L1/L2/L3/L4": kept honest -- Python/NumPy cannot touch the CPU's actual +hardware caches; "L1..L4" here is an ANALOGY for a hot(full)/warm(delta)/cold(recompute) frame-storage hierarchy, +not cache-line control. MEASURED, with the kept NEGATIVE: a local 5-row bump moving through a 100-row array caches +9x smaller than full-frame storage; a 64x64 gaussian WAVE that touches most vertices per frame only 1.4x -- the +saving scales with the LOCALITY of the per-frame change (big for sculpt/brush/local sim, ~full-size for a global +deformation, as it must be, since a global change genuinely is new data every frame). + +**The last classic mesh tools (holographic_meshtools.py).** mirror (reflect across a plane, reverse winding so +normals stay consistent, weld the seam) and merge_by_distance / weld (snap-to-grid group via np.unique, mean per +group via an np.add.at scatter, remap the (T,3) face table and drop degenerates as array ops -- vectorised for +triangle meshes; a polygon fallback loop for n-gons). Verified: weld fuses duplicate verts and drops collapsed +faces; mirror is symmetric about the plane and welds the seam. (solidify deferred -- it needs boundary-edge +bridging for open meshes, more than a one-shot vertex offset; flagged not faked.) + +All exposed as faculties: deform (bend/twist/taper dispatcher, Mesh OR point cloud), lattice_deform, blend_shapes, +timeline, frame_cache, bake_deformation, mirror_mesh, weld_mesh. The deformers and blendshapes take a Mesh or a +raw (N,3) array, so the same call animates geometry, a particle cloud, or (via the field bridge) a volume's +sample positions. + +## Solidify, and field-native lighting -- AO, soft shadows, HDRI sky, refraction, SSS, GI, caustics (+16 tests, 1849→1865) + +The ask: finish solidify, then refraction / caustics / subsurface scattering / ambient occlusion / global +illumination / HDRI skydome, "all as composable native VSA things." THE HONEST FRAMING KEPT LOUD: these are +LIGHT-TRANSPORT effects, not hypervector algebra, and the modules do not pretend otherwise. They belong in this +engine because holostuff is SDF/FIELD-native, and on a field these effects are cheap and composable -- the field +answers the only questions they ask (nearest-surface distance, occlusion along a ray, the gradient/normal, +interior path length). Each per-ray quantity is vectorised over all rays (loops are over march STEPS, ~tens, not +pixels). The GENUINE VSA/engine connections, named where true: an accumulation is a scatter = a bundle; the GI +irradiance cache IS the engine's adaptive-anchor sparse-cache idea; SSS is the field interior integrated; Snell's +law is called OPTICS, not dressed up. + +**solidify (holographic_meshtools.py).** Offset an inner copy along vertex normals, reverse its winding, and +bridge the boundary edges so an OPEN sheet becomes a watertight solid (a closed mesh becomes a hollow double +wall). Vectorised offset; the bridge loops over boundary edges only. Verified: open 36-face sheet -> 120-face +watertight, manifold solid. + +**Field-native SDF lighting (holographic_raymarch.py).** A vectorised CPU sphere-tracer (the SDF value is the +safe step), plus: sdf_normal (gradient, 6 evals); ambient_occlusion (Quilez -- march the normal, read the field; +no hemisphere rays); soft_shadow (Quilez -- march toward the light, track closest approach); sky_dome (procedural +HDRI sky+sun+ground OR an equirectangular HDRI array sampled by lon/lat -- the incoming radiance is a bundle of +directional sources); refract_dir (Snell, TIR->reflect -- OPTICS); subsurface (march the light direction through +the SDF interior, Beer-Lambert transmission -> thin parts glow); render_sdf composes them (direct*shadow + +ambient*AO + fresnel env reflection + refraction + SSS, sky-dome background). Measured: AO darkens a crease +(0.63) vs open floor (1.00); soft shadow under a sphere 0.00 vs 1.00; refraction bent 86% of the glass-sphere's +pixels; SSS brightened the thin torus. KEPT NEGATIVE: refraction is a SINGLE-surface approximation (front-face +only, no exit-interface or internal path) -- a tinted/frosted look, not true two-interface glass; honest, not +faked. + +**GI + caustics (holographic_globalillum.py) -- the engine's real contributions.** GLOBAL ILLUMINATION via a +sparse IRRADIANCE CACHE: gather one-bounce indirect (cosine-hemisphere secondary traces) at n_cache surface +points, inverse-distance interpolate the rest -- Ward's irradiance caching = the engine's adaptive-anchor idea. +MEASURED: a 24-point cache reconstructs the 144-point dense GI at mean err 0.004 (indirect light is smooth, so it +caches cheaply -- the genuine win). CAUSTICS by FORWARD light tracing: shoot parallel light rays, refract those +that hit the object, splat where they land on the receiver with np.add.at -- the scatter that IS the bundle (the +adjoint of sampling). Where refracted rays converge the bundle piles up: MEASURED caustic peak 138-702x mean (a +sphere lens focuses to a bright spot + refraction ring, visually confirmed). KEPT NEGATIVE: the splat is +point-wise so it shows the discrete light-ray grid; a Gaussian splat kernel (which the engine already has, +splat_fit/aniso_render) would smooth it. + +All exposed as faculties: solidify_mesh, render_sdf, ambient_occlusion, soft_shadow, sky_dome, refract, +subsurface, irradiance_cache + read_irradiance, caustics. THE BOTTOM LINE: the engine doesn't make NumPy a GPU +path tracer; it makes these effects cheap because it is field-native, and it contributes two real accelerations +the literature names -- the sparse irradiance cache (GI) and the scatter/bundle light splat (caustics). + +## Landmark (Nystrom) spectral embedding -- lifting the dense-eigh "moderate N" wall (+6 tests, 1865→1871) + +The ask (a sharp one): the engine's "moderate N" limit comes from a dense eigh/svd; instead of a global dense +matrix, do high-precision eigendecompositions on local "hot" manifolds and treat the background as a coarse +low-rank approximation -- reusing the IRRADIANCE-CACHE logic for the latent space itself. + +PROBE-FIRST grounded it exactly: the SVD calls (consolidation, rate-distortion, denoise) are on (N,D) data +matrices -- roughly LINEAR in N, so clustering buys little there. The real O(N^3) wall is +`holographic_spectral.laplacian_eigenbasis`, which runs np.linalg.eigh on the full N x N graph Laplacian and +computes ALL N eigenvectors to keep the lowest few. `spectral_basis`'s own docstring already said "Dense eigh -> +moderate N" -- that was the note the user referenced. + +THE METHOD is Nystrom (Fowlkes et al. 2004, *Spectral Grouping Using the Nystrom Method*), which IS the +irradiance cache applied to the latent space: the leading eigenvectors of a smooth affinity are smooth and +low-rank, so (1) pick m << N LANDMARKS that cover the data (farthest-point sampling = every cluster/local +manifold gets an anchor, the discrete cousin of the engine's blue-noise sampling), (2) do the high-precision eigh +on the small m x m landmark block (the expensive computation, on the anchors), (3) EXTEND to all N by the Nystrom +formula (the cheap interpolation = the coarse background). Cost O(N^3) -> O(m^3 + N*m); only an N x m affinity +block is ever formed, never N x N. `holographic_nystrom.py`: farthest_point_landmarks, gaussian_affinity (blocks +only), nystrom_embedding (degree estimated by the Nystrom factorization so even D=W@1 never forms W), +dense_embedding (the reference), subspace_alignment (the sign/rotation-invariant quality metric). + +MEASURED: + * COST (the headline, exactly as O(N^3) vs O(m^3+Nm) predicts, m=64): N=300 4.7x; N=600 41x; N=1200 80x; + N=2400 **286x faster, 38x less memory** -- and the win GROWS with N, so the wall genuinely moves toward large + N. (dense 3773 ms / 46 MB vs nystrom 13 ms / 1.2 MB at N=2400.) + * QUALITY (the kept NEGATIVE): EXACT for low-rank / well-separated structure (3 blobs: subspace alignment to + dense = 1.000); only ~0.62->0.76 on a curved Swiss-roll manifold as m goes 16->128 -- a higher-rank affinity + makes Nystrom an APPROXIMATION with diminishing returns in m. It trades exactness for scale; use the dense + `spectral_basis` when N is small and exactness matters, Nystrom when N is large. + * COVERAGE: on IMBALANCED data (800-pt + 30-pt clusters) FPS landmarks align 0.841 +/- 0.031 vs random + 0.765 +/- 0.130 -- FPS is more accurate and FAR more stable, because random landmarks can miss the small + cluster entirely. This is why "cover the hot manifolds" (FPS), not uniform-random, is the right anchor rule. + +Faculties: nystrom_embedding (the scalable companion to spectral_basis; same return shape, drop-in for the +smooth-embedding use), spectral_landmarks (FPS coverage set). The honest bottom line: the user's move is sound and +it is the irradiance cache by another name; it lifts the dense-eigh ceiling by ~2 orders of magnitude in N at the +cost of exactness on high-rank manifolds -- measured, with both the win and the bound on the record. + +## A capacity-adaptive 3D holographic octree -- tiling the wave when one vector is too full (+6 tests, 1871→1877) + +The ask wove together four ideas: (1) reuse the engine's TILING to scale 3D/sim; (2) keep things as composable +VSA programs; (3) a "wave" can describe many particles, and when a vector is "too full" spin up another and use a +wave to reference both; (4) use the diff/delta machinery so a split structure references the original. + +PROBE-FIRST mattered enormously here -- most of this already exists, and rebuilding it would be the canonical +failure mode: + * TILING for capacity is shipped in 2D: `splat_bundle_tiled` + `recall_region_tiled` keep each tile bundle + bounded so recall holds at any resolution ("one vector per tile -- the price of exceeding a single vector's + capacity"). `TiledStore` + `_tile_bucket` (holographic_tree) are the shared routing primitive. + * The "WAVE" is the FPE VectorFunctionEncoder: bundle points into f = sum encode(p_i); cosine(f, encode(x)) + reads a kernel-density occupancy. `FPEField` (FS-5) already carries geometry as one wave with edit=bind and + DELTA editing (make_delta/apply_delta/remove_delta) -- the "reference the original, store the diff" mechanism, + already linear and O(change). + +So the GENUINE GAP was narrow and worth building: the existing tiling is 2D and FIXED-size; what was missing is a +3D, CAPACITY-ADAPTIVE auto-split. `holographic_octree.py` (HoloOctree): a 3D octree whose every node carries its +points as one FPE wave and SUBDIVIDES into 8 octants the moment its count exceeds `capacity` -- "spin up another +vector when the first is too full," automatic and in 3D. The tree IS the bidirectional index (descend a position +to its leaf = forward; read the leaf's points/occupancy wave = backward), and each child encoder is scaled to its +smaller box so local resolution sharpens with depth (the same local-refinement logic as the Nystrom landmarks). + +MEASURED (the capacity cliff, and the fix), AUC = P(stored score > empty score), 1.0 perfect / 0.5 chance, dim +fixed at 2048: + * a SINGLE global wave works at small N and COLLAPSES as N grows: AUC 0.85 (N=50) -> 0.60 (200) -> ~0.5 (800+) + -- past capacity one vector literally cannot tell a stored point from empty space. + * the capacity-adaptive OCTREE holds AUC ~0.9-1.0 at every N by splitting (8 -> 64 -> 330 -> 512 leaf vectors as + N goes 50 -> 6400), bidirectional lookup and per-leaf <= capacity verified. + * the honest COST: one wave hypervector per non-empty leaf -- storage grows ~N/capacity, the same trade the 2D + tiling makes. Tiling does not add information capacity for free; it spends proportional storage to keep each + vector inside its budget. + +HONEST framing on the four ideas, kept loud: + * "a wave describes infinitely many particles" -- TRUE only as resolution-independent SAMPLING (a continuous + field you can query at any x); FALSE as information content. A wave is finite-capacity (the cliff), which is + exactly WHY we tile/split. The octree is that fix. + * "spin up another vector + a wave references both" -- the octree's auto-split + the tree-as-index across the + per-node waves IS that, made concrete. + * "VSA programs / composable" -- each node is an FPE wave (VSA), composable; but "runs better" is the same NumPy + underneath. What makes it SCALE is the tiling, not a speed trick. + * "delta to split" -- the FPE bundle is LINEAR, so a node's wave is the sum of its points' deltas and an edit is + a make_delta/apply_delta (already in FPEField); splitting redistributes those deltas. Reused, not rebuilt. + +Faculty: holo_octree(bounds, points, capacity, dim, bandwidth, ...). The bottom line: the user's instinct was +right and mostly already instantiated; the new piece is the 3D capacity-adaptive auto-split, and it moves the +single-vector capacity cliff out indefinitely at proportional storage cost -- measured, with the cost on record. + +## Void-capability-gap program synthesis -- synthesize, verify, gate-or-abstain (+7 tests, 1877→1884) + +The suggestion: when the registry finds no suitable tool (a "void capability gap"), let the system SYNTHESISE its +own program; treat assembly as constrained optimization in the latent space; verify the program vector against +the goal before execution; refine until coherent; and (claimed unlocks) blend programs, and a cross-domain +"synesthesia." + +PROBE-FIRST found almost all of it already shipped -- the third proposal in a row that was mostly in the box: + * the orchestrator's plan() already DETECTS the void gap (a backward typed search returns (None, 'gap')); + * `optimize_toolchain` already does "program assembly as constrained optimization in latent space" -- gradient + ASCENT on cosine(chain_signature, goal). HONEST REFRAME of "backpropagate": that gradient is a HAND-DERIVED + analytic expression through the softmax tool-selection (numpy only) -- NOT autodiff, NOT learning. The engine + has no autodiff (hard constraint); "the machine backpropagates its instruction sequence" is this analytic + cosine-ascent over which tools to pick; + * `synthesize_procedure` already does the discrete cousin (bounded BFS over VM ops, VERIFIED BY EXECUTION before + return, None if unreachable); + * validators exist (validate_recipe, recipeops.validate). + +THE GENUINE GAP was the bridge: nothing connected plan()='gap' to synthesis with a VERIFY -> GATE-or-ABSTAIN loop. +`holographic_voidsynth.py`: synthesize_for_goal optimises a chain over a library toward the goal, GROWS the length +if a short program won't reach (the structural 're-bundle'), VERIFIES the DISCRETE chain's coherence (never trusts +the soft optimum), and GATES -- returns status 'synthesized' if coherence >= threshold, else 'abstain'. Plus +blend_programs (bundle two program signatures) and fill_capability_gap (registry-hit short-circuit, else synthesise). + +MEASURED: + * THE GATE (the load-bearing safety property): 20/20 REACHABLE goals synthesized (mean coherence ~1.00), 20/20 + UNREACHABLE goals (random, independent of the library) ABSTAINED (mean best coherence ~0.19). It cleanly + separates fillable gaps from genuine voids -- it NEVER returns an incoherent program as if it solved the goal. + This abstention is the whole point: filling a void gap with junk would be worse than admitting the gap. + * REFINEMENT: the latent ascent converges -- often FAST (coherence 1.000 by 5 steps for cleanly-reachable + goals), so the verify-and-abstain GATE, not the optimizer's persistence, is what makes synthesis safe. (Honest: + no dramatic multi-step climb on easy reachable goals; the optimizer is not the hard part, the gate is.) + * BLEND / "SYNESTHESIA": a blended program stays coherent to BOTH source goals (~0.72/0.74 for a graphics + program bundled with an audio one). Because every domain's tools live in the SAME vector space, one program + can carry two intents. That is what "synesthesia across domains" actually is -- the project's one-algebra + thesis (bind/bundle/cleanup is domain-agnostic), NOT a mystical new sense. Honestly named. + +Faculties: synthesize_program, blend_programs, fill_capability_gap. KEPT NEGATIVES: synthesis only reaches goals +in the library's reachable span (an unreachable goal abstains -- correctly); the optimizer can sit in a local +optimum of a non-convex landscape; "backprop" is analytic gradient ASCENT on a known cosine, not autodiff and not +learning; the blend is lossy superposition (coherence ~0.7, not 1.0, to each source). The bottom line: the +suggestion was sound and largely already built; the new, load-bearing piece is the verify-gate-ABSTAIN bridge that +turns "no tool found" into either a verified synthesised program or an honest decline. + +## An upgraded creature agent -- affect, a pain reflex, and void-gap action synthesis (+8 tests, 1884→1892) + +The ask: upgrade the basic creature mind now that it can drive VSA programs -- define actions, give it inputs like +reward/pain, and whatever else makes it effective beyond a maze NPC. The headline upgrade (flagged last round): +wire the SYNTH-1 verify-gate-abstain loop into decide so a void gap in the AGENT (no learned action fits) triggers +ACTION-PROGRAM synthesis. + +WHY A NEW LAYER, NOT AN EDIT to HolographicMind: the existing RL engine is deterministic and TIE-SENSITIVE (a +1e-16 change flips a maze trajectory, its own kept-negative), and its bespoke per-action prototype value memory +measurably beats a generic memory (0.96/0.75 vs 0.57/0.25). So `holographic_agent.py` (Agent) ADDS capabilities +around it, backward-compatible, rather than disturbing that suite. + +WHAT IT ADDS: + * ACTIONS AS VSA ATOMS -- each action is a near-orthogonal hypervector, so a plan has a composed signature + (chain_signature), which makes actions SYNTHESISABLE and a plan EMBEDDABLE in / blendable with other VSA + programs (the agent can DRIVE a program -- the thing the user noted is now possible). + * AFFECT: reward AND pain -- reward folds +value into the (state->action) memory; pain folds -value AND records + the (state, action) for a faster avoidance channel. + * PAIN REFLEX -- before consulting values, drop any action that strongly resembles a remembered painful one. A + SAFETY reflex, faster than value learning (measured: ONE pain event blocks the action; no convergence needed). + * VOID-GAP ACTION SYNTHESIS (the headline) -- when no allowed action is confidently recognised here (support + below value_floor: the agent's OWN void gap) and a goal is given, synthesise an action PROGRAM toward the + goal, verify its coherence, and COMMIT it if it clears the threshold else ABSTAIN to a safe default. The + creature analogue of filling a registry void: compose a plan rather than flailing, but only if it verifies. + * SELF-EXPLAINING -- decide returns the source ('value'/'synthesized'/'abstain'/'explore'), what it avoided, and + WHY. + +MEASURED: affect learning works (state with E rewarded / N painful -> chooses E, avoids N; a second state -> +chooses its reward, avoids its pain); the pain reflex blocks an action after a SINGLE event; void-gap synthesis is +15/15 reliable both ways (reachable goal -> a synthesised plan, unreachable -> abstain to a safe default); a plan's +signature blends with another program at cosine 0.86 (the agent drives a program). KEPT NEGATIVES: the value memory +here is a simple soft-kNN (the bespoke engine stays in HolographicMind); synthesis only reaches goals in the action +library's span (abstains otherwise, correctly); deterministic and tie-aware. + +A DETERMINISM CATCH worth recording: the agent builds its atoms from default_rng(seed); a test that drew its +"random unreachable goal" from default_rng(0) while the agent used seed=0 produced a goal IDENTICAL to the 4th +action atom (cosine 1.0) -- so synthesis correctly found it reachable, and the "failure" was a seed COLLISION in +the test, not a bug. Fixed by drawing the test vectors from an independent seed. Exactly the class of bit-exact +determinism subtlety the engine's tie-break discipline exists for. + +Faculty: agent(actions, dim, seed, value_floor, pain_reflex, synth_threshold). The bottom line: the creature is no +longer a reactive maze NPC -- it has affect (reward/pain), a safety reflex, a self-explaining policy, and the +ability to SYNTHESISE and verify a multi-step plan when it hits a situation it has no learned action for, with all +of it expressible as VSA-program atoms. + +## Homeostatic drives -- scheduling faculties through a nested process (+7 tests, 1892→1899) + +The ask: a drive/homeostasis layer so the agent can DRIVE denoising, pattern recognition, and descent decisions +through deeply nested / fractal processes that are otherwise hard to operate on by hand (too many decision points +to schedule manually). Probe-first: no drive/need/homeostasis system existed (genuine gap); the faculties to drive +all did (denoise = codebook cleanup, recognise/recall with calibrated abstention, decompose_nested/fractal_*). + +`holographic_drives.py`: DriveSystem -- homeostatic needs {clarity, understanding, coverage, energy}, each a level +in [0,1], starting DEFICIENT (a satisfied drive exerts no pull -- starting them satisfied was the first cut's bug +that saturated everything to 1.0). `pressing(applicable)` returns the most weighted-deficit need whose faculty can +act at this node. make_nested_process builds a heterogeneous tree (some nodes recognisable-but-noisy, some pure +noise, some deep) with CALIBRATED noise (scaled 1/sqrt(dim) so the signal is recoverable, not buried -- another +first-cut bug: raw noise was sqrt(dim)x too large, so denoising couldn't help and nothing was recognisable). +drive_process walks the tree on a tight energy budget, at each node applying the faculty its most-starved drive +selects, with REAL faculties and a genuine DEPENDENCY: recognition only succeeds on a CLEANED signal, so clarity +ENABLES understanding and the schedule must interleave them. + +MEASURED (negatives kept loud): + * the faculties work: denoise lifts max-cosine-to-codebook ~0.39 -> ~1.0 (gain ~0.6) per node; cleaned nodes + then recognise; pure-noise nodes never do (correctly). + * THE HONEST RESULT: across heterogeneous trees and budgets, the drive schedule MATCHES the best fixed-priority + schedule WITHOUT being told which order is right (drive balance ~0.46 vs best fixed ~0.45; best-or-tied on + ~23/24 trees), and BEATS naive scheduling 2-4x on the worst-served need (random ~0.17, descend-first ~0.04). + It does NOT beat a well-chosen fixed priority, because the denoise->recognise dependency plus applicability + already force most of the good ordering. So the value is ROBUSTNESS / an adaptive default: you do not have to + hand-pick the schedule for a process too nested to script, and the drives self-tune to whatever is starved. + * the metric is `balance` = the WORST-served task drive (min over clarity/understanding/coverage) -- the honest + homeostatic objective (keep every need above water), which a policy that maxes one need and starves another + scores low on even if its mean looks fine. + +KEPT NEGATIVES: drives are a SCHEDULER over existing faculties, not a faculty improver; on a uniform process or one +with an obvious fixed order a fixed priority ties them; they need setpoints/weights (a tuning knob); random has high +variance and can occasionally beat drives on an easy single tree (the win is on AVERAGE over heterogeneous trees). +Two first-cut measurement bugs found and fixed loudly: drives starting satisfied (no pull -> saturation), and noise +scaled sqrt(dim) too large (signal buried -> denoise gain 0, nothing recognised). Faculties: drive_system, +drive_process. The bottom line: an adaptive, self-explaining default scheduler for denoising/recognition/descent +through nested processes -- matching the best hand-picked schedule without knowing it, and well clear of naive ones. + +## register_apply_handler -- faculties (incl. octree/nystrom/agent) callable from VSA programs (+7 tests, 1899→1906) + +A backlog of 8 items arrived ("most of it is wiring up what exists"). Probe-first triage against the live code: +ALREADY/PARTIAL -- recall/compose programs (recall_procedure + learn_procedure + fingerprint recall exist), +verification (verify_chain/validated/validate_recipe exist; self-correction = a thin wrapper); GENUINE GAPS -- +exposing faculties as APPLY handlers (only cleanup/denoise/matmul wired), run_chunked threads ONLY the accumulator +(registers + permute-stack do not cross a chunk boundary), trace->program abstraction, dreaming-synthesis, +approximation demos; DOC -- writing_vsa_programs.md exists, needs the new capabilities. The KEYSTONE (cheapest, +highest-value, the intended extension point) was item #1. + +The APPLY mechanism already existed: `APPLY ` means ACC := faculty(ACC), run by a HOST that supplies a +handler dict; the bare VM has none. The mind exposed only cleanup/denoise/matmul/datafit/diffuse, and the +docstring literally said "extend this dict." `register_apply_handler(name, fn)` is that extension point generalised: +any unary acc->acc closure -- INCLUDING stateful spatial ops (an octree query, a Nystrom approximation) and agent +behaviours, since the closure captures the built octree / fitted embedding / Agent -- becomes a programmable +`APPLY ` step. It registers a faculty atom on the VM so APPLY's operand cleans to the name, and merges the +handler into the live set (consistent with the existing set_matmul / set_inverse_problem / generator pattern). + +MEASURED: a registered handler run as `APPLY ` inside a program produces cosine 1.0 vs calling the faculty +directly; demonstrated with a Nystrom landmark projection (fast approximation), an Agent behaviour (acc=state -> +the agent's learned action vector -- chose the rewarded action at cosine 1.0), and an octree-style spatial recall; +handlers CHAIN in order; a registered name overrides a built-in; non-callables are rejected. This is the bridge +from "the agent drives a program" (AGENT-1) to "a program drives the engine": a synthesised or hand-written VSA +program can now denoise, recall, query space, approximate, or act, all inline. + +KEPT HONEST: APPLY's contract is UNARY acc->acc, so faculties that are not vector->vector (the DriveSystem +scheduler, multi-arg ops) do not fit as bare handlers -- they belong in the host loop, or wrapped behind a closure +that fixes their extra arguments (as the agent_act closure does). The demo's first cut had two TEST bugs, found and +fixed loudly: a leading LOAD overwrote the seeded accumulator, and a trailing APPLY cleanup snapped an octree- +recalled vector back to a codebook atom -- the dispatch was correct, the test programs were wrong. Faculty: +register_apply_handler. Remaining backlog for next sessions: run_chunked register/stack threading (#5), the doc +refresh (#7, partially started here), and the research-y items (trace->program #3, dreaming-synthesis #4, +approximation-in-large-sims #8). + +## run_chunked threads the FULL state across seams + a composable state continuation (+6 tests, 1906→1912) + +Backlog #5. Probe confirmed the gap: `run` created fresh `regs={}` / `stack=None` each call and returned only +(acc, trace), so `run_chunked` (which calls run() per chunk) threaded ONLY the accumulator -- a register STOREd in +one chunk was gone by the next, and PUSH/POP could not span a seam. Guided by "VSA native WHEN beneficial," this +was built in two honest halves: + +THE EXACT HALF (the correctness fix). `run(..., init_regs, init_stack, return_state=False)` -- additive, +backward-compatible (default still returns the 2-tuple; recursive CALL/ITERATE sub-runs keep their local register +scope). `run_chunked` now carries the accumulator AND the register dict AND the stack across each seam, each in its +EXACT representation. MEASURED: a register stashed in chunk 1 and recalled in chunk 4 comes back cosine 1.0; PUSH in +one chunk / POP in a later one restores the value 1.0. Deliberately NOT bundled per-seam: bundling the register file +into one vector at every boundary would inject crosstalk that COMPOUNDS over a long program -- the exact dict adds +none. This is the "VSA-native is NOT beneficial here" call, made explicitly. + +THE VSA-NATIVE HALF (the composability win). `state_to_vector(acc, regs, stack)` bundles the whole machine state -- +accumulator + register file + stack, each bound to its role (new unitary ACC/STK roles + the existing reg-name +codebook) -- into ONE composable hypervector: a CONTINUATION. A paused computation becomes a first-class VALUE you +can STORE in memory, recall, compose, or resume -- the same "a program/state is just a vector" composability the +inception layer uses, now for execution state. `state_from_vector` unbinds each role and cleans against a codebook. +MEASURED: acc + 3 atom-valued registers round-trip EXACT-after-cleanup (1.0, registers bit-identical). KEPT NEGATIVE +(loud): the RAW pre-cleanup readback degrades as slots are packed -- 0.49 (2 regs) -> 0.41 (4) -> 0.32 (8), the +~1/sqrt(#slots) capacity cliff -- so the bundled continuation is exact only for cleanup-able (atom) slots, lossy for +arbitrary continuous values, and is for SNAPSHOT/compose, not the hot per-seam carry (which is why that stays exact). + +So the principle "composable VSA-native programs give compounding benefits" is honored precisely: VSA-native for the +continuation (state as one storable/composable vector -- the compounding win), exact for the per-seam thread (where +bundling would compound crosstalk instead). Both measured. No new top-level faculty (these are HoloMachine methods +the procedure faculty already exposes via run/run_chunked); 312 tests across the touched ISA/machine modules green. +Remaining backlog: doc refresh (#7, extended here), trace->program (#3), dreaming-synthesis (#4), approximation in +large sims (#8). + +## Chunked delta chain with a hash-chain + Merkle integrity proof (+9 tests, 1912→1921) + +The request: for chunked DATA, store deltas from the first chunk (base) or the prior chunk (or both), not full +data, with a proof/fractal thing for integrity + propagation; use the tiered cache + codebooks; stay optimized and +VSA-native/exposed; avoid VSA<->Python hot spots; and make sure recent improvements are wired. + +WIRING AUDIT (done first): synthesize_program / blend_programs / fill_capability_gap / agent / drive_system / +drive_process / register_apply_handler are all referenced in UnifiedMind; state-threading lives in the machine; +voidsynth/agent/drives referenced 12x. Nothing siloed. + +PROBE-FIRST: FrameCache (anim) already stores frames as deltas vs a BASE with a hot/warm/cold tier (the HONEST +L1-L4 analogy -- Python cannot touch real CPU caches); scenedelta content-hashes COMPONENTS for dedup. Neither does +delta-vs-PRIOR, auto base/prior selection, or a CHAINED integrity proof over a sequence -- the genuine gaps. + +`holographic_deltachain.py` (DeltaChain): append a sequence of (N,D) chunks; each stored as a delta vs the BASE or +the PRIOR, whichever has fewer changed rows (auto). A SHA-256 HASH CHAIN folds each chunk into the prior's hash +(propagation), and a binary MERKLE ROOT over all chunk hashes is the single 'fractal' proof of the whole sequence. +get(i) reconstructs AND verifies -- a tampered delta / wrong base / broken upstream propagation raises +IntegrityError instead of silently returning garbage. + +MEASURED (negatives kept): bit-exact reconstruction; a DRIFTING sequence auto-chooses prior-deltas 9/10 (small +incremental edits), a NEAR-BASE sequence chooses base-deltas 10/10 -- the auto-selection adapts; ~9.7x smaller than +storing every chunk full on a 12-chunk drift; tamper DETECTED by the hash chain; the Merkle root is deterministic +(same sequence -> same root) and change-sensitive (any edit -> different root). CODEBOOK compression is LOSSLESS +(changed rows that EXACTLY equal an atom store an 8-byte index, not a D*8 float row) -- but its TOTAL win is base- +capped on short sequences (~1.3x, the base dominates) and only materialises when deltas dominate: on a 150-chunk +D=256 sequence it is 5.2x total, 86x on the DELTA portion, 144x vs storing full. KEPT NEGATIVES: codebook win is +base-capped for short sequences; an atom-row is exact only if it EQUALS the atom (near-atom rows fall back to full, +no silent loss); and -- the recurring discipline -- EXACT INTEGRITY IS hashlib, NOT a VSA bundle: a bundled checksum +is lossy and cannot give bit-exact tamper detection, so this is a case where VSA-native is NOT beneficial and the +exact hash is the right tool. VECTORIZED throughout (changed-row detection = np.where over a max-abs reduction, +codebook match = a broadcast compare, reconstruct = fancy indexing, hash = one call per chunk on .tobytes()): the +only Python loop is over CHUNKS, so there is no hot VSA<->Python seam on the data. Faculty: delta_chain. A natural +next step the request points at: have run_chunked RECORD its per-seam states +into a DeltaChain, giving a verifiable, O(change) replay log of a long program's execution. + +## Replay log + trace->program + nystrom-field + dreaming (four builds, +10 tests, 1921→1931) + +A batch of four, each probe-first, measured, negatives kept. + +### REPLAY LOG (run_chunked -> DeltaChain) -- the connector the last two builds set up. +run_chunked gained record=True: after each seam it snapshots the FULL state as ROWS (state_rows: acc + R0..R7 + +stack, one row each, zeros for empty) and returns the sequence. Faculty execution_replay wraps that sequence in a +DeltaChain -> a verifiable, O(change) execution log. WHY rows not the bundled state vector: consecutive seams share +most rows (registers that didn't change), so the row-delta is small; the bundled (1,D) state would be one ever- +changing row and delta-compress to nothing. MEASURED: bit-exact reconstruction of every seam; ~4.7x smaller than +storing all states full; resumable (the acc row recovers); tampering with the log is DETECTED by the hash chain. + +### TRACE -> ABSTRACT PROGRAM (#3) -- abstract_program (a mind method over synthesize_procedure). +From a TRACE = a set of (input, output) examples, synthesise a procedure on the first and VERIFY it on ALL the rest; +the abstraction is the program CONSISTENT across examples, stored by name. MEASURED: an abstracted [BIND key] +transfers to a HELD-OUT input at cosine 1.0, vs a prototype-nearest-neighbour at -0.01 -- the program captures the +TRANSFORM (transfers), the prototype only matches near-identical states (returns a stale output). KEPT NEGATIVE: +abstracts only transforms expressible in the VM's ops within max_depth, and returns generalizes=False (not a wrong +program) when the examples share no such transform (tested). + +### NYSTROM FIELD APPROXIMATION (#8) -- nystrom_kernel_apply / faculty nystrom_field. +Approximate a kernel-weighted field f(p)=sum_j w_j K(p, src_j) (Gaussian RBF) via m LANDMARK sources -- +K(points,sources) ~ C pinv(W) B, never forming the full kernel -- O((Np+Ns)m) not O(Np Ns). ONE tool for a PHYSICS +field (particles+charges) and LARGE MEMORY (items+payload, queries). MEASURED on a smooth potential: corr 0.998 +(N=800) / 0.995 (N=2000), speedup 4x->13.5x (grows with N). KEPT NEGATIVE: exact only for a LOW-RANK (smooth) field; +a high-frequency field (tiny sigma -> near-identity kernel, full rank) drops to corr 0.22 -- use the exact sum or +more landmarks there. HONEST on the grouping: voidsynth is a PROGRAM-synthesis tool, not a field approximator, so it +was NOT shoehorned into #8; the approximation win is Nystrom's. + +### CONSOLIDATION + DREAMING (#4) -- holographic_dream.py / faculties consolidate_subspace, dream. +Ties consolidation (the low-rank subspace real states live on) to B10's denoise-from-noise generation, plus the two +asks: NYSTROM approximates the consolidation subspace from m farthest-point LANDMARK memories (O(mDk) not O(NDk)) for +a large store; DREAMING = generative replay over the CONSOLIDATED subspace (draw noise -> project onto the subspace +-> optional cleanup), producing samples ON the manifold (valid) yet NOVEL (not stored atoms) -- novel COMPOSITIONS, +the regime B10 flagged as interesting (bare-codebook generation just returns atoms). MEASURED: landmark subspace +aligns 1.000 to the full subspace on low-rank memory; dreamed samples on-manifold ~1.0, novelty ~0.12. KEPT NEGATIVE: +the Nystrom subspace is exact only when the memory is genuinely LOW-RANK -- on full-rank noise a landmark subset +misses directions (alignment < 0.8, tested); dreaming recombines within the consolidated span, it does not invent +outside the manifold. + +Faculties: execution_replay, abstract_program, nystrom_field, consolidate_subspace, dream. The recurring discipline +held: probe-first (synthesize_procedure, consolidation, B10 generate, the nystrom landmarks all already existed -- +the gaps were the row-snapshot replay glue, the cross-example verification, the kernel-apply factorization, and the +subspace-projection dream pass); every win paired with the regime where it does NOT hold. + +## Stable-fluids solver: smoke / buoyancy / fire (FLUID-1, +8 tests, 1931→1939) + +The request: make sim/materials/render "comparable to V-Ray/Bifrost/Redshift/Houdini/ZBrush in CAPABILITY." +Honest framing held first: pure NumPy CANNOT match their compiled+GPU throughput; what CAN be made comparable is +the ALGORITHM/METHOD (the offline brain). Capability audit found NO Navier-Stokes solver existed (holographic_flow +is the Tero slime-mold graph solver; holographic_transport is Wasserstein OT) -- the single biggest, most on-thesis +gap. Built it. + +holographic_fluid.StableFluid -- Stam 'Stable Fluids' (SIGGRAPH 1999), the method Houdini's smoke solver and +Bifrost Aero are built on. 2D or 3D (shape sets the dim). Carries velocity + smoke density + temperature + fuel, so +ONE solver does smoke, buoyant plumes, and combustion/FIRE. + +WHY on-thesis (Jos Stam, advisory panel): + * PRESSURE PROJECTION is a Helmholtz-Hodge decomposition done in the FOURIER domain -- u_hat -= k(k.u_hat)/|k|^2 -- + a circular convolution on the periodic grid, the SAME algebra as bind. The pressure solve other engines do with + hundreds of Jacobi sweeps is ONE pair of FFTs here, exact. This is the engine's periodic-domain structure doing + the most expensive step of every pro fluid solver for free. + * SEMI-LAGRANGIAN advection: backtrace + interpolate -> unconditionally stable (the 1999 contribution). + +KEY CORRECTNESS SUBTLETY (kept): the projection must use the SYMBOL OF THE CENTRED DIFFERENCE (kappa=sin(theta), +since 0.5*(roll(-1)-roll(+1)) acts as i*sin(theta)), NOT the ideal k=theta. We measure divergence and vorticity with +centred differences, so projecting with the matching symbol makes the velocity divergence-free in the TRUE discrete +sense (residual 6.7e-16) instead of leaving a finite-difference residual (the first cut left div~1.2 -- a real bug, +found and fixed loudly). DC and the centred-diff null modes (Nyquist, where sin=0) get |k|^2:=1 and are left +unchanged -- correct, since they are invisible to the divergence operator. + +MEASURED (negatives kept loud): + * incompressibility: projection drives max|div| 3.66 -> 6.7e-16 (machine precision), in 2D and 3D. + * stability: dt=2.0 (CFL-violating) over 50 steps stays finite + divergence-free relative to |v| ~3e-16; an + explicit solver would NaN. Constant forcing with no viscosity grows |v| unbounded -- correct physics, not an + instability. + * buoyancy: a hot plume's centre of mass rises ~24 cells. + * combustion/FIRE: fuel above ignition is consumed (64 -> 0.1), releases heat (sustains), yields smoke. + * vorticity confinement: keeps ~88x more enstrophy (swirl) than OFF -- the detail term that makes the flame curl. + * performance (HONEST, offline brain): 128^2 ~10 ms/step (near-interactive in 2D), 64^3 ~0.5 s/step (~2 fps), + 0.5M cells/s in 3D. The METHOD matches the pros; the throughput does not, and we say so. + * KEPT NEGATIVE: semi-Lagrangian advection is DISSIPATIVE -- ~20% smoke mass lost over 60 steps to interpolation + smoothing; a MacCormack/BFECC or FLIP scheme conserves better and is the honest next step. Boundaries are + periodic (the FFT projection's price; solid obstacles need a separate masking solve). + +Rendered a sustained fire plume to /mnt/user-data/outputs/fluid_fire_plume.png (temperature -> blackbody ramp, +density -> smoke) -- the curling flame is the vorticity confinement at work. Faculty: fluid_solver. + +REMAINING capability gaps in the other three areas (sequenced, NOT yet built): MATERIALS -- a Cook-Torrance/GGX +microfacet BRDF (raymarch currently has only Schlick Fresnel + flat reflection); RENDERING -- a Monte-Carlo path +tracer for true multi-bounce GI (current globalillum is a single-bounce irradiance cache); GEOMETRY is already +well covered (euler ops, poly ops, curvature, geodesics, LOD), so lower priority. + +## Materials + rendering: Cook-Torrance/GGX BRDF and a Monte-Carlo path tracer (BRDF-1 + PATHTRACE-1, +10 tests, 1939→1949) + +Finishing the sim/materials/render parity push. After the fluid solver, the two remaining genuine gaps were +MATERIALS (raymarch had only Schlick fresnel + flat reflection) and RENDERING (globalillum was single-bounce only). +Honest framing unchanged: method-parity in NumPy, NOT GPU speed-parity. + +### COOK-TORRANCE / GGX BRDF (holographic_brdf.py) -- the V-Ray/Redshift/Arnold reflectance model. +f_r = diffuse + D*G*F/(4 NdotV NdotL): GGX/Trowbridge-Reitz normal distribution (D), Smith/Schlick-GGX geometry +(G), Schlick Fresnel (F, F0=0.04 dielectric / base_color metal). Diffuse is Lambert scaled by (1-F)(1-metallic) so +specular+diffuse never exceed incoming (energy split; metals have no diffuse). Plus an importance sampler +sample_ggx (H ~ D(H)NdotH, reflect -> pdf = D NdotH/(4 VdotH), so brdf/pdf cancels D and the 1/4NdotV NdotL) and a +one-sample-MIS sample_brdf (mix a cosine-diffuse and a GGX-specular lobe, evaluate the COMBINED mixture pdf at the +chosen L -> unbiased regardless of which lobe drew the sample). Wired into render_sdf as an opt-in pbr=(metallic, +roughness) path (backward-compatible; default keeps the legacy shade). MEASURED: white-furnace reflectance ~0.98 +(energy-conserving), GGX importance sampler unbiased (estimator matches brute-force integration). KEPT NEGATIVE: +single-scatter GGX loses energy at high roughness (white-furnace dips below 1); Kulla-Conty multiscatter +compensation is the next step. Rendered pbr_material_sweep.png: rough dielectric / smooth dielectric (tight +highlight + Fresnel rim) / gold metal (tinted reflection, no diffuse). + +### MONTE-CARLO PATH TRACER (holographic_pathtrace.py) -- true multi-bounce GI, the core of V-Ray/Redshift/Arnold. +Solves the full rendering equation over an SDF scene: follow random light paths, at each hit sample a bounce from +the BRDF (sample_brdf), multiply throughput by f_r*cos/pdf, continue until a ray escapes to the emissive +environment or RUSSIAN ROULETTE ends it (terminate weak paths with prob tied to throughput, divide survivors by it +-- unbiased). VECTORISED over rays: all H*W rays march together each bounce, the Python loop is only over the few +bounces. Indirect light (color bleeding, soft GI) falls out for free -- the thing single-bounce irradiance caching +can't do. MEASURED: white-furnace convex sphere converges to its ALBEDO (0.590 vs 0.6 -- unbiased, single-scatter +slack); noise falls as 1/sqrt(spp) (0.064@8spp -> 0.026@64spp); color bleed reproduced (red floor -> sphere +underside red/green 1.56 -> 2.46 going from direct to 5-bounce). PERF (HONEST): 128^2/96spp ~13-16s -- the OFFLINE +renderer, NOT Redshift RT's interactive GPU path tracing. KEPT NEGATIVE: no next-event estimation -- light is +gathered only when a bounce hits the emissive ENVIRONMENT, so a big sky converges well but a small bright emitter +would be very noisy; NEE/MIS-with-lights is the honest next step. Inherits the GGX single-scatter energy loss. +Rendered pathtrace_gi.png (left direct, right full multi-bounce GI with the floor's red bleeding onto the sphere). + +Faculties: path_trace (+ render_sdf gained the pbr= material path). With the fluid solver, this closes the +sim/materials/render capability-parity push: a Navier-Stokes smoke/fire solver, a physically-based BRDF, and a +multi-bounce path tracer -- method-parity with the pros, honest throughout that pure NumPy is the offline brain and +the GPU stays the realtime muscle. GEOMETRY was already well-covered (euler/poly ops, curvature, geodesics, LOD), +so it was correctly NOT rebuilt. + +## GPU backend (optional CuPy) + architecture above/below sweep (BACKEND-1 + SWEEP-1, +9 tests, 1949→1958) + +Two asks: (1) let the user enable a GPU (CuPy) backend, ideally GPU for the parts that benefit and NumPy for the +rest; (2) an above/below sweep for hot spots where VSA programs call Python and a VSA-native/vectorised version +would be better. + +### THE SWEEP'S HEADLINE: batched VM execution (run_batch) -- a real Python-loop hot spot, fixed. +The VM decodes each instruction once (unbind + nearest-atom lookup -- the expensive per-instruction work) and the +value ops (bind/bundle/permute) are elementwise/FFT over the last axis. So running ONE program over N data items +meant N full Python interpret passes, re-decoding every instruction N times. HoloMachine.run_batch threads an +(N, D) accumulator and decodes ONCE, applying each op to all rows with the batch-correct primitives. MEASURED: +N=2000, 9-instruction straight-line program -> per-item run() loop 7537ms vs run_batch 709ms = **10.6x**, matching +the scalar VM to max|diff| 8.5e-17. Faculty run_procedure_batch. ROOT CAUSE worth recording: bind() hardcodes +n=a.shape[0] and permute() uses np.roll with no axis -- both silently corrupt a 2-D batch (bind returned (5,5)), +i.e. they were written 1-D-only; bind_batch and roll(axis=-1) are the batch-correct forms, which is what run_batch +uses. SCOPE (kept honest): straight-line value+register programs only; control/host ops (IFMATCH/CALL/APPLY/...) +diverge per item and raise a clear error rather than return a silently-wrong batch. + +### THE REST OF THE SWEEP (honest, humbling): the architecture is ALREADY vectorised. +A scan of the hot modules (unified, resonator, sbc, archive, forest, creature) found almost NO per-iteration +bind/cosine inside Python loops -- bind_batch, involution_batch, recognize_batch, step_vec etc. already exist. The +'replace Python loops with vectorised scatter' lesson has already been applied across the stack. So the sweep's +honest output is ONE genuine remaining hot spot (the VM, now batched), not a long list -- the codebase was already +disciplined. Reported plainly rather than inventing work. + +### GPU BACKEND (holographic_backend.py) -- optional CuPy, NumPy fallback, follow-the-data. +get_array_module(x) returns cupy if x lives on the GPU else numpy (mirrors cupy's helper); a kernel writes +`xp = array_module(self.device)` once and uses plain xp.fft/xp.zeros/...; to_device/asnumpy move data at the +boundary; on_gpu_island wraps a heavy kernel so the caller passes/receives NumPy while the compute runs on the +device. enable_gpu/use_gpu toggles it (env HOLOSTUFF_GPU=1 too). WHY selective, not 'import cupy as np everywhere': +GPU only wins where host<->device transfer is amortised over a lot of compute (big FFT/matmul kernels); a tiny +per-call op on one (D,) vector LOSES to transfer cost -- so heavy kernels opt in, the rest stays NumPy (exactly the +'GPU-friendly parts on CuPy, rest on NumPy' split the request asked for). Wired into the FLUID SOLVER as the +flagship: StableFluid(device='gpu') runs the whole FFT-heavy sim on the device; device='cpu' (default) is +byte-identical to before (verified). Faculties: use_gpu, backend_status. + +KEPT NEGATIVE (loud): (1) DETERMINISM -- GPU FFTs/reductions match NumPy only to a tolerance and can vary +run-to-run, so the bit-exact guarantees are a CPU property; GPU mode is for throughput, the tie-sensitive paths +stay on CPU. (2) UNMEASURED HERE -- this sandbox has no GPU and cupy isn't importable, so the GPU path is wired and +code-reviewed but the speedup is NOT measured here; it's measured in a CUDA environment via HOLOSTUFF_GPU=1. +Everything is verified on the NumPy fallback (what runs with no device). The run_batch 10.6x, by contrast, is a +real CPU measurement and needs no GPU. + +## Extraction from leOS: the Riemannian geometry layer (SPHERE-1, +6 tests, 1958→1964) + +holostuff is the extracted core of leOS (github.com/AnOversizedMooseWithSocks/leOS) and will fold back in. Surveyed +leOS for dependency-light (NumPy/Flask/stdlib/hashlib-only), genuinely-missing, on-thesis capabilities across the +asked areas (kernel/learning/text/orchestration/planning). PROBE-FIRST as always -- most candidates turned out to +be already ported or already covered: + * vsa/superposed_compute (speculative multi-hypothesis) -- holostuff already has holographic_superposed + (pack/recover_all/score_all/evaluate_candidates); the speculate/curriculum parts are agent-coupled. COVERED. + * lvm/reflex_engine + the displacement codec -- holographic_ai already credits and ports "leOS's displacement + codec" (log_map/exp_map reflex arc). COVERED. + * science/fractal_detector -- holostuff already has holographic_fractal (box_counting_dimension, + image_fractal_dimension, fractal_dimension faculty); the cross-scale IFS-rule part is coupled to leOS's + displacement log. MOSTLY COVERED / app-specific. + * vsa/residue_arithmetic, vsa/program_algebra -- overlap holostuff's RNS matmul and voidsynth blend_programs; + partial, lower-value. + * lvm/imagebind_* , embeddings -- depend on torch/models. NOT portable (banned deps). + +THE GENUINE GAP: leOS's lvm/spherical_geometry had two operations holostuff lacked despite having the basic maps +(geodesic/log_map/exp_map/slerp already in holographic_ai). Extracted into holographic_sphere.py: + * frechet_mean (Karcher mean) -- the geometrically-correct average: the point minimizing the sum of squared + GEODESIC distances, via Riemannian gradient descent (normalized Euclidean mean -> step along the averaged + log-maps via exp_map). The Euclidean centroid of sphere points isn't on the sphere, and re-normalizing it + (what bundle does) is biased on the curved surface. This is the right op for a class PROTOTYPE / cluster centre + / consolidation anchor -- DISTINCT from bundle (superposition, stays similar to every part, for binding). + * parallel_transport -- carry a tangent vector (a displacement/move) from one base point to another along the + geodesic, so it lives in the destination's tangent plane; lets displacements be composed/compared across the + space correctly. + geodesic_variance (the dispersion the Frechet mean minimizes). + This is the project's "VSA is geometry / as above, so below" thesis made rigorous. Pure NumPy, reusing + holostuff's own log_map/exp_map so the algebra is identical to the rest of the engine. + +MEASURED (negatives kept LOUD): the Frechet mean provably has lower geodesic variance than the re-normalized +Euclidean mean (its defining optimality), differs from bundle by ~0.16 rad for a SPREAD set vs ~0.0004 for a TIGHT +one (so the geometry only "pays" when vectors are genuinely spread), and parallel transport preserves length and +lands in the target tangent plane (verified). KEPT NEGATIVE: the downstream-task edge is MARGINAL -- on +nearest-prototype classification of spread-but-overlapping classes the Frechet prototype was ~tied-to-slightly- +behind the Euclidean one (both near chance when classes overlap), so the reliable value is the provable geometric +optimality + transport correctness, NOT a free accuracy win. We did not oversell a classification benefit that +isn't reliably there. Faculties: frechet_mean, parallel_transport. + +## Extraction from leOS (deep dig): local structure classification / cosmic web (COSMIC-1, +6 tests, 1964→1970) + +Asked to dig DEEPER into leOS -- gems may hide under unassuming names. Mapped every pure-NumPy/stdlib module +(filtering out torch/scipy/sklearn-dependent ones) and read the algorithm files, not just the obvious ones. The +kernel mixins (kernel_vsa/kernel_core_instructions/kernel_advanced/kernel_reflex) turned out to be thin wrappers +over libraries holostuff already has (bind/bundle/resonate/residue/superpose/program-algebra/displacement-codec). +The genuine gem was hiding in science/cosmic_web.py. + +THE GAP: holostuff had GLOBAL dimension estimates (box_counting_dimension, spectral_dimension, manifold_topology) +but no PER-POINT LOCAL structure classification. Extracted into holographic_cosmic.py (decoupled from leOS's agent +displacement-log so it works on any (N,D) cloud): the 'cosmic web' method -- classify each point by the eigenvalue +spectrum of a LOCAL PCA of its k nearest neighbours into VOID / FILAMENT (1-D thread) / WALL (2-D sheet) / NODE +(dense cluster), the same structure-tensor classification cosmologists use on the matter distribution. ON-THESIS: +"structure across scales / as above, so below." Useful before denoising (project a filament point along its ONE +direction, not all), before sampling (avoid voids), or as a compact geometric fingerprint of a cloud. + +IMPROVEMENT over the extracted version: a continuous PARTICIPATION RATIO intrinsic dimension PR = (sum lambda)^2 / +sum(lambda^2) (1.0 = filament, ~2 = sheet, ~d = isotropic) alongside the discrete type -- both a crisp label and a +graded measure. + +MEASURED (negatives kept LOUD): on structures of KNOWN dimensionality embedded in 32-D, the intrinsic dim recovers +MONOTONICALLY -- filament 1.00 < sheet 1.75 < blob 2.52 -- and a 1-D cloud reads 85% filament, a 2-D cloud 78% +wall/node. KEPT NEGATIVE: high-dimensional NOISE inflates the apparent local dimension (the 1-D line jumps to PR +3.20 at noise 0.01), and the estimate depends on k and the cloud's density -- a local estimate, honestly bounded, +not a magic dimensionality oracle. The 3-D blob reads ~2.5 not 3.0 (finite-sample under-estimate of k points in +3-D). Faculties: local_structure, classify_cloud. + +Other leOS candidates noted for later (not extracted this pass): lvm/gravitational_lens (force-directed gradient +navigation + caustic detection in embedding space, pure NumPy -- a plausible next extraction); kernel_reflex's +conformal_calibrate (conformal prediction -- holostuff's RecallNull/calibration_report already covers the coverage +guarantee, so lower marginal value). The embedding/baseline-calibrator/drift-detector stacks depend on torch and +stay out by the dependency rule. + +## Extraction from leOS (deep dig cont.): gravitational-lens navigation + caustic detection (LENS-1, +6 tests, 1970→1976) + +Second extraction from the deep leOS dig (lvm/gravitational_lens.py), decoupled from its nutrient-field so it +works on any set of weighted attractor points. Built holographic_lens.py. + +WHAT IT ADDS: treat stored points as MASSES on the hypersphere; a query feels a force toward them (each pulls along +the geodesic / log_map direction with strength mass * Gaussian(geodesic^2/2sigma^2)). field_force sums them; +deflect slides the query along the force via exp_map -- a SOFT continuous cousin of cleanup (drift toward the +weighted local centre of mass, vs cleanup's hard snap to one atom); navigate iterates a decaying-step climb to an +attractor. The genuine gem is CAUSTIC DETECTION: a caustic (optics: a fold where rays focus and the map goes +singular) here is a query point where the two strongest attractors pull in OPPOSITE directions with similar +magnitude -- a routing fold / decision boundary where a tiny move flips the winner. Complementary to RecallNull +('is this a match at all?'); caustic asks 'is this AMBIGUOUS between matches?'. + +MEASURED (negatives kept LOUD): deflection moves a query nearer its closest attractor (1.34 -> 1.24 rad in one +step). CAUSTIC is the crisp win: the MIDPOINT between two attractors scores 1.000 (perfect tie pulling apart), +a query near a single attractor scores 0.000 -- a clean ambiguity detector. KEPT NEGATIVES: (1) navigate is a +HEURISTIC DRIFT, not an exact nearest-cluster solver -- a fixed step overshoots the well (strength 2.0 diverged to +1.83 rad), so it needs a decaying step (strength/(1+0.1t)) to settle, and even then it APPROACHES (~0.02-0.15 rad) +rather than converging crisply below tol; (2) sigma is the scale knob with no free lunch -- wide sigma over-smooths +(pulls toward the global centroid), narrow sigma barely moves. (3) The force is a DIRECT O(N) sum, not the +Barnes-Hut O(N log N) tree leOS used -- exact and clear, but the large-N acceleration (which HoloForest could +supply) is deliberately omitted. Faculties: field_deflect, detect_caustic, navigate_field. + +leOS deep-dig status: the two clean pure-NumPy gems (cosmic_web local structure, gravitational_lens navigation) +are now extracted. Remaining leOS modules are either already covered (kernel mixins over existing libs), app-glue +(agent/bots/infra/knowledge orchestration, LLM-coupled), or torch/scipy-dependent (embeddings, baseline_calibrator, +drift_detector, math_engine). No further clean extractions identified. + +## Numba integration: optional JIT + fast-sweeping eikonal SDF (JIT-1, +6 tests, 1976→1982) + +Moose approved adding Numba. Integrated it the SAME way as the CuPy backend -- as an OPT-IN accelerator with a pure +fallback, so the constitution's portability + determinism guarantees survive. Built holographic_jit.py. + +THE PATTERN: try/except import; if numba is absent, `njit` becomes an IDENTITY decorator and the same kernel source +runs as ordinary (slow) Python -- the core never hard-depends on numba. DETERMINISM: plain @njit only, NEVER +parallel=/fastmath= (those two are the only Numba features that break bit-exactness), and the selftest PROVES the +JIT output equals the pure-Python output (allclose 1e-9). + +WHY ONLY HERE: re-confirmed by probing live code that holostuff's hot paths are already vectorized (sphere_trace +loops over steps but vectorizes over rays and calls a Python SDF closure numba can't cross; _sample_periodic is a +vectorized multilinear gather). Numba's measured elementwise gain is only ~1.4x -- not worth a dep. The exception +is a SEQUENTIAL recurrence with a data-dependent branch (measured ~33x earlier). The showcase kernel is exactly +that shape and genuinely on-thesis: the FAST-SWEEPING EIKONAL SOLVER (occupancy mask -> signed distance field). +It is inherently sequential (Gauss-Seidel sweeps read neighbours updated earlier in the same pass -- that is what +makes it O(N) not O(N^2)), so it does NOT vectorize, and an SDF is the heart of the modelling/raymarch/sculpt +vision. Pure NumPy had no fast occupancy->SDF path; now it does. + +MEASURED: disk SDF max error 0.96 cells vs the analytic (r-R) signed distance (correct); JIT == pure-Python +(bit-faithful, deterministic); SPEED pure 602 ms -> JIT 2.2 ms = 273x on a 256^2 grid. Faculties: +signed_distance_field, distance_transform. requirements-accel.txt added (numba; cupy noted) and referenced from +requirements.txt, keeping the base install at NumPy/Flask/stdlib. KEPT NEGATIVE / scope: 2-D only (3-D fast +sweeping is the natural extension, 8 sweep directions); first-call JIT warmup ~30ms per signature per process +(amortized over a run, a real tax on very short runs); fast sweeping gives the exact grid distance in the round +limit -- a couple of rounds suffice for typical fields but a thin seed set may want more. + +## SymPy design-time codegen: exact SDF normals / forces (CODEGEN-1) + optional pyFFTW backend (FFT-1) -- +8 tests, 1982→1990 + +Both panel follow-ons from the dependency discussion. Built holographic_codegen.py and holographic_fft.py. + +### CODEGEN-1 (the panel's "unlock", Quilez + Baker seats) +holographic_codegen.py: a DESIGN-TIME SymPy helper that derives an exact gradient symbolically and lambdifies it to +a PURE-NUMPY function -- runtime stays pure and autodiff-free, only the one-time derivation touches sympy. (Named +codegen, NOT symbolic: probe-first caught that holographic_symbolic.py is already the MDL symbolic-REGRESSION module +-- the opposite direction, discovering a law FROM data. Different capability, different name.) compile_field(expr, +vars) -> value/gradient fns + the symbolic partials; sdf_normal_fn(expr) -> exact unit normal (Quilez: replaces the +finite-difference sdf_normal, no step knob); gradient_fn (Baker: force = -gradient(energy), analytic, autodiff-free). +MEASURED: sphere exact-normal error 1.0e-12 (machine precision) vs finite-difference 5.7e-05 at step 1e-2 -- ~7 orders +better and no step-size knob; torus normal matches numeric to 2.2e-10; force=-grad(quadratic well) exact. Gated: +HAS_SYMPY; helpers raise a clear message without sympy; the OUTPUT is pure numpy and ships without it. Faculties: +exact_sdf_normal, symbolic_gradient. + +### FFT-1 (the panel's pyFFTW suggestion -- MEASURED, kept off) +holographic_fft.py: an FFT backend seam (numpy default, pyFFTW opt-in) wired into bind/bind_batch/bind_fixed. The +DEFAULT numpy path is BYTE-IDENTICAL (np.array_equal verified; 328 core bind-exercising tests pass unchanged). The +HONEST MEASURED RESULT and the reason it is OFF by default: pyFFTW REGRESSES at holostuff's operating dimensions -- +single binds ~0.65x at D=512, ~0.75x at D=1024, break-even ~D=2048, winning only at D>=4096 (~1.6x); and the BATCHED +bind path (which the engine relies on) is often WORSE (~0.5x at M=4000, D=2048) because numpy's batched pocketfft is +already well tuned and FFTW threading/planning adds overhead. It is also tolerance-not-bit-exact (~3e-14). This is +EXACTLY the C-kernel-PR lesson (an external compiled backend regressing at the operating point) -- kept on the record, +not discovered twice. The seam still earns its place: future-proofs for D>=4096 workloads and makes the measurement +reproducible (holographic_fft.benchmark / faculty fft_benchmark). numpy stays the deterministic default; switching is +an explicit opt-in (use_pyfftw / faculty fft_backend). KEPT NEGATIVE: pyFFTW is a net regression at our dims -- wired +as an honest off-by-default option, not an endorsement. + +requirements-accel.txt updated: sympy (design-time codegen), pyfftw (commented, off-by-default). Core install stays +NumPy/Flask/stdlib. + +## Runtime compile cache: compile once, cache by content hash, reuse everywhere (COMPILE-1, +7 tests, 1990→1997) + +Moose's idea: use sympy (and compilation generally) at RUNTIME, not just dev time -- a system that compiles a thing, +caches the compiled version for reuse all over, and recompiles only when the underlying thing changes. Probe-first +confirmed holostuff has tiered STORAGE caches (FrameCache, DeltaChain -- the hot/warm/cold "L1-L4" analogy) but NO +general COMPILE cache. Built holographic_compile.py. + +THE JUSTIFICATION (measured): compiling a symbolic SDF to a numpy normal (sympy diff + lambdify) costs ~140-390 ms; +EVALUATING the result on 1000 points costs ~200 us -- the compile is ~1900x an evaluation. Anything that compiles a +spec per-use (an SDF re-lambdified every frame, a VSA program re-assembled every run, a recipe rebuilt each time) +pays that cliff repeatedly. The cache turns N compiles into 1. + +THE PRIMITIVE: CompileCache -- an LRU cache of compiled artifacts keyed by a DETERMINISTIC sha256 (hashlib, never +salted hash()) of a CANONICAL representation of the source. Content-addressing IS the invalidation: a changed source +canonicalises differently -> misses -> recompiles automatically; "if the underlying thing changes, we compile it +again" falls out for free. get_or_compile(source, compiler, tag); a global DEFAULT_CACHE so subsystems share +artifacts; compiled() as the general entry point structures/programs/encoders/SDFs can all call. Application wired: +compiled_sdf_normal (compile a symbolic SDF normal once, reuse instantly). Faculties: compiled_sdf_normal, +compile_cache_stats. + +MEASURED: 50 uses of one spec -> 1 compile + 49 hits (not 50x the compile); recompiles on change; LRU bound holds +(maxsize evicts coldest); the ~140 ms SDF compile paid ONCE then 20 reuses in 0.1 ms (would have been ~2.8 s of +recompiles). KEPT NEGATIVES (the hard part of any cache is invalidation, stated honestly): correctness depends on the +KEY capturing every dependency the artifact has -- the compiler must be a pure function of `source`; a compiler that +closes over external state not in the key would go stale (caller's contract, documented). Memory is bounded by +maxsize (compiled artifacts, esp. Numba kernels, are not free). A hit returns the SAME object to all callers -- so it +is for PURE compiled functions; sharing a stateful artifact this way would be a hazard. This is the runtime leg of +the SymPy->NumPy/Numba pipeline: derive once, compile once, cache, reuse. + +## Two cache-backed compilers: SymPy->Numba SDF + VSA program assembler (COMPILE-2, +6 tests, 1997→NNNN) + +Both plug into the COMPILE-1 cache. Added sdf_numba_fn to holographic_codegen.py; compiled_sdf_numba + +compiled_program to holographic_compile.py. + +### SymPy -> Numba SDF (the full pipeline leg) +sdf_numba_fn(expr): compile a symbolic 3-D SDF to njit SCALAR value + exact-normal functions and njit grid +evaluators. THE UNLOCK: the scalar njit SDF can be CALLED FROM ANOTHER njit loop -- the Python-closure barrier that +earlier blocked Numba from sphere_trace (it called a Python SDF closure njit couldn't cross) is GONE. Demonstrated: +a njit sphere-trace marching by calling the njit SDF hit a sphere R=1.3 at t=3.700 (exact). MEASURED: grid_normal +matches analytic to 1e-10; the njit scalar loop (9.5 ms / 200k pts) BEATS numpy-vectorized (22.6 ms) AND a python +scalar loop (~153 ms, ~16x) -- numba wins for scalar-heavy/sequential eval by avoiding numpy's temporaries. NOTE: +njit of a lambdified function must NOT use cache=True (no source file -> "no locator" error); the compile cache +handles reuse instead. 3-D only (SDFs are 3-D). Cached via compiled_sdf_numba: both the sympy lambdify AND the numba +JIT (each costly) are paid once per distinct SDF. Faculty: compiled_sdf_numba. + +### VSA program assembler (cached) +compiled_program(machine, program): assemble() encodes an L-instruction program into ONE vector via L binds + a +bundle -- measured ~15-22 ms for 60 instructions. Running the SAME program repeatedly over different inputs/batches +re-paid that each time; now it is cached by (program ops, machine seed/dim) and reused. MEASURED: 1 compile ~22 ms, +50 reuses ~5 ms total (vs ~1.1 s uncached, ~240x less), cached == fresh assemble (byte-identical). Recompiles when +the program or machine identity changes. Faculty: compile_program. + +BUG FOUND + FIXED during close-out: CompileCache.clear() reset the store but NOT the stats counters, so the shared +DEFAULT_CACHE leaked `compiles` counts across tests (passed in isolation, failed in sequence). Made clear() a FULL +reset (store + stats). A reminder that shared global caches need clean reset semantics for deterministic tests. + +## njit analytic-SDF renderer wired into render_sdf (SDFRENDER-1, +6 tests, 2003→NNNN) + +The end-to-end payoff of SymPy->Numba: a fully-JIT'd renderer for ANALYTIC SDFs. Built holographic_sdf_render.py. + +THE UNLOCK REALIZED: the numpy render marches rays calling sdf.eval(P) (a Python closure njit can't cross), which is +why Numba could never touch it. With sdf_numba_fn the SDF + gradient are njit functions, so the WHOLE march -- +primary ray, exact normal, ambient occlusion (Quilez: march along the normal), soft shadow (Quilez: march toward the +light) -- compiles into ONE njit kernel per pixel, closing over the njit SDF (inline call, faster than passing it as +an arg). build_sdf_renderer(sdf_numba) -> njit render(O,D,L,base,ambient,do_ao,do_shadow); compiled_sdf_renderer(expr) +caches it per SDF; render_analytic(expr, camera, ...) is the drop-in: njit shades hits, numpy composites the sky for +misses, returns (H,W,3). + +WIRED EVERYWHERE USEFUL: render_sdf gained an opt-in jit_expr= param -- pass a symbolic SDF string and it routes to +the njit renderer for the field-native shading (Lambert + soft shadow + AO + sky), falling back to numpy automatically +if sympy/numba are absent or if advanced features (pbr/reflect/refract/sss) are requested (the jit path scopes to the +basic set, documented). Faculty: render_sdf_fast. + +MEASURED: njit renderer matches the numpy sphere_trace hit geometry 100.0%; a 160x160 sphere with AO + soft shadows +renders in ~17 ms vs ~148 ms for numpy render_sdf = ~9x (15x at 200x200 -- the gap grows with resolution since the +numpy path scales worse). KEPT NEGATIVE / scope: covers the basic field-native shading only (no pbr/reflect/refract/ +sss -- those stay on the numpy path); 3-D SDFs; first-SDF compile pays the sympy lambdify + numba JIT once (cached +thereafter). + +PRE-EXISTING BUG FOUND + FIXED: render_sdf's non-pbr branch crashed with ao=False or shadows=False (it did +(ambient*occ)[:,None] assuming occ/sh were arrays, but they are scalar 1.0 when those effects are off). Fixed to +handle the scalar case (occ_c = occ[:,None] if np.ndim(occ) else occ), byte-identical for the default array case. +Found because the new test exercised that path -- tests earning their keep. + +## Optimization sweep: compound SDFs + exact gradient cache (SWEEP-1, +6 tests, 2009→NNNN) + +Asked to sweep the whole engine -- auto/harmonic, creature/agent, simulations, geometry, denoising -- for places the +SymPy/Numba/compile-cache toolkit adds speed or accuracy. The DISCIPLINED finding (measured, not assumed): most named +areas are ALREADY vectorized, so Numba buys nothing there -- forcing it would be cargo-culting. The genuine new wins +are two specific niches, both shipped: + +1. COMPOUND SDFs (extends the njit renderer). Added SDF combinators to holographic_codegen.py: sphere, box, + op_union (Min), op_intersect (Max), op_subtract (Max(a,-b)), op_smooth_union (Quilez polynomial smin). These build + SymPy expressions that the EXISTING sdf_numba_fn + render_analytic compile and render -- no new machinery. The + refactor that made it robust: some compound gradients (nested Min/Max/Abs, e.g. the box) leave an unprintable + sympy Derivative, so sdf_numba_fn now tries the EXACT symbolic normal and FALLS BACK to a njit FINITE-DIFFERENCE + normal on the scalar SDF when the symbolic gradient won't compile (exposed as exact_normal: bool). MEASURED: a + compound scene (sphere union box, carved by a sphere) renders 200x200 in ~58 ms vs ~668 ms numpy = 11x, hit + geometry matching 100%; a plain sphere keeps its EXACT normal, the box scene uses the FD fallback (still njit-fast, + unit normals, invisible error for rendering). Faculty: render_sdf_fast already accepts compound expressions. + +2. EXACT GRADIENT CACHE (accuracy). gradient_cache_symbolic(expr, anchors): build the irradiance/GI-style + GradientCache with EXACT Jacobians from a symbolic field (SymPy) instead of central finite differences. MEASURED: + Jacobian error vs analytic -- finite-diff (eps=1e-3) 1.6e-7, symbolic-exact 0.0 -- so first-order interpolation is + more accurate at the same anchors. Faculty: gradient_cache_symbolic. + +HONEST SWEEP VERDICTS (kept, so the negative is on record): + * auto self-balancing harmonic (holographic_accumulate): ALREADY OPTIMAL -- harmonic 1/n weights converge by + design (the EMA flatlines); nothing to JIT, the accuracy is already the right one. + * creature / agent (decide/step/run_episode): object/dict-based, NOT numba-able without a rewrite; the hot path + (VSA recall) is already a vectorized matmul. No win. + * fluid: already vectorized + device-aware (CuPy backend). flow (Tero): already linear-algebra-based (~100x over + the ant). denoise: SVD + sublinear HoloForest recall, already vectorized. marching_tetrahedra_vec: already + vectorized. No numba win in any -- the earlier vectorization sweep already did this work. + * geometry SDF: covered by the eikonal njit SDF (JIT-1), the analytic-SDF renderer (SDFRENDER-1), and now compound + SDFs. This is where the toolkit genuinely pays. + +The meta-lesson, consistent with the whole project: the toolkit's gains live in the SEQUENTIAL/SYMBOLIC niches (SDF +marching, eikonal sweeps, exact gradients), NOT in re-JITing code that NumPy already vectorizes. A sweep that honestly +reports "already optimal" everywhere else is doing its job. See holostuff_optimization_sweep.md for the full findings. + +## 3-D eikonal SDF + composable post-processing pipeline (EIKONAL3D + POSTFX, +17 tests, 2015→2032) + +Two features: the 3-D twin of the eikonal SDF, and a post-processing "projection tail" for the rasterized frame. + +### 3-D fast-sweeping eikonal (EIKONAL3D) +holographic_jit gained _fast_sweep_3d (8 diagonal sweeps; the 3-D Godunov upwind solve adds dimensions one at a time +from the smaller neighbour on each axis -- sort a<=b<=c, try 1-D then 2-D then 3-D), distance_transform_3d, and +signed_distance_3d (occupancy VOLUME -> signed distance, negative inside). The natural Numba target: inherently +sequential, no vectorized form. MEASURED: 3-D ball SDF 0.96-cell error vs analytic; JIT == pure bit-exact; a 96^3 +volume -> SDF runs 14.8 s pure -> 65 ms JIT = 229x. This is the occupancy-volume -> SDF step mesh import and sculpt +want in 3-D (signed_distance_2d was 2-D only). Faculty: signed_distance_field_3d. + +### Post-processing pipeline (POSTFX) +holographic_postfx.py: an optional, ordered, named PROGRAM of effects (PostChain) composed onto the (H,W,3) frame as +the last step of projection -- the same shape as a HoloMachine instruction sequence (.then() to build, + to compose, +to_list/from_list to serialize, .apply(img, depth=) to run). The HONEST framing kept throughout: + * The CONVOLUTION FAMILY (gaussian blur, bloom, glare, DOF, denoise, sharpen) runs on the engine's OWN core + operator. bind(a,b)=irfft(rfft(a)*rfft(b)) is 1-D circular convolution; a 2-D blur is irfft2(rfft2(img)*G) -- the + SAME operator, one dimension up, with a frequency-domain Gaussian (no kernel truncation). Bloom/glare are a + SUPERPOSITION (bundle) of blurred bright layers. This is a genuine VSA connection, not a metaphor. + * The per-pixel curves (exposure, reinhard/ACES tonemap, gamma, color_grade, vignette, film_grain, chromatic + aberration, lens_flare) are plain vectorized NumPy. They live in the same pipeline because that is where a frame + gets graded -- but calling them "VSA" would be dishonest; they are the rest of the program. Said so in the docs. +Effects: exposure, reinhard, aces, gamma, color_grade, vignette, bloom, glare, lens_flare, chromatic_aberration, dof +(needs depth -- the renderer now returns a depth buffer), motion_blur, denoise, sharpen, film_grain (seeded +deterministic), resample (bi-linear up/down), supersample (SSAA). Presets: default_chain, cinematic_chain. + +INTEGRATION: the njit render kernel now also returns a DEPTH buffer (ray distance t at the hit; 1e30 = miss), so +render_analytic and render_sdf both gained post= and return_depth=. DOF reads that depth. MEASURED on a smooth-union +scene 320x320: raw frame mean 0.388 / std 0.281 (dark linear, clipping) -> default_chain mean 0.640 / std 0.124 +(gamma-encoded, ACES-tonemapped, bloomed, vignetted) -- the grading visibly lifts and tames the raw buffer. The FFT +blur is energy-preserving (DC conserved to 1e-6); film grain is bit-identical for a fixed seed. Faculties: +post_process, postfx_chain. + +KEPT NEGATIVES (loud): + * Deeply NESTED compound SDFs are slow to njit-COMPILE. A smooth_union(sphere,box) carved by a subtract builds a + huge lambdified math expression; the first compile TIMED OUT (>300 s). A plain smooth-union of two spheres + compiles in ~2 s. Lesson: keep analytic-SDF scenes shallow, or accept a long one-time compile -- the cost is in + the SymPy->math->numba lowering of a giant expression, not the render. (The numpy render_sdf path has no such + compile cost.) + * DOF is a CHEAP two-level blend (sharp vs one blurred copy by circle-of-confusion), not a true scatter/gather + bokeh -- no shaped aperture, no per-depth blur radius beyond the blend weight. Honest game-grade DOF, not + reference. + * motion_blur / glare are LINEAR (single direction); no per-pixel velocity buffer, so no curved or + object-specific motion blur. Camera-style only. + * The convolution-family blur is CIRCULAR (FFT wraps at the frame edge); a very large bloom sigma can bleed a + bright edge to the opposite side. Fine at normal radii. + +The meta-point: the post-processing is genuinely composed as part of the projection (a program on the frame, the +convolution family on the engine's own operator), and the parts that are just colour math are labelled as such. + +## Controlled semantic scene layer: text -> queryable VSA scene -> 3-D (SEMANTIC-1, +9 tests, 2032->2041) + +Moose's idea: the long-dormant text/learning stack should assign SEMANTIC VALUES that other parts of the engine can +predict/generate/query bidirectionally -- e.g. a sentence -> a 3-D scene, query a scene for objects/materials, batch +ops like "make all materials reflective", and UI control specs. PROBE-FIRST found the scaffolding already there: +holographic_lang (S-expr structure language -> recipe IR), holographic_text (learn_word_vectors, n-grams), +holographic_scenegraph (SceneNode/transforms -> recipe), and the encode_record/decode_record record codec. The gap +was the SEMANTIC layer ABOVE lang's S-expressions: ground words, parse a description, realize to actual SDF geometry. + +holographic_semantic.py adds that, on the existing substrate: + * GROUNDING tables (word -> meaning): SHAPES (ball/sphere->sphere, cube->box), COLORS (->rgb), MATERIALS + (metallic->metal, glassy->glass), SIZES (elongated->stretch), RELATIONS (inside/on/leaning/diagonal). + * PARSER (controlled grammar, deterministic, one-token lookahead): articles a/an = introduce an object, the = + refer back ("the glass box" resolves to the existing glass box); pre-mods ("red ball") and post-mods ("box with + a glass MATERIAL"); first relation word wins ("leaning" over "on"). MEASURED on the exact example sentence: parses + to 3 objects (red sphere INSIDE box1; glass box1; metal elongated box2 LEANING on box1) + environment + {sun:bright, sky:partly} -- correct. + * VSA ENCODE/QUERY (the on-thesis win): each object is a bind/bundle RECORD (encode_record); the scene is ONE + composable hypervector = superpose bind(OBJ_i, record_i). MEASURED: every attribute of every object decodes back + out of the SINGLE bundled scene vector via unbind+codebook-cleanup -- 12/12 correct through the superposition + crosstalk. That is the bidirectional, content-addressable semantic memory Moose wanted, and it is composable (the + scene vector can be handed to the agent brain, recalled, or edited). + * BATCH ops: batch_set("material","mirror") = "make all materials reflective" in one call; find_objects(material= + "glass") locates by attribute. + * REALIZE + RENDER: objects -> positioned SDF primitives with per-object materials (glass->refract, metal->pbr, + mirror->reflect), z-composited; relations drive layout (inside -> at container centre, shrunk). Rendered the + example sentence text -> 3-D in ~3 s, plus a batch "make all mirror" variant. + * UI CONTROL SPECS: control_spec("control the ball size and how metallic it is") -> slider/select descriptors + scoped to a target ("ball"), ready for a front-end to draw. Engine emits the spec; the browser muscle draws the + widgets. + +Faculties: parse_scene_description, encode_scene, query_scene_slot, render_scene_description, scene_control_spec. + +SCOPE / KEPT NEGATIVES (loud, in the spirit of lang's "kept boundary"): + * This is a CONTROLLED vocabulary + keyword grammar, NOT general language. No learned model (the engine has no + torch/learned weights): "red" is an rgb because the table says so, synonyms are folded by the table, anything + outside the vocabulary is glue/ignored. The VSA part (bidirectional record query, composable scene vector) is the + genuine, hard-to-fake contribution; the language surface is honestly narrow. learn_word_vectors COULD later + ground synonyms by corpus relatedness instead of by hand -- a real next step, not claimed now. + * RENDER is per-object z-composite, so objects do NOT cast shadows on each other and a glass object does NOT + refract the object behind it (the red ball "inside" the glass box won't show through). A single-pass material-id + renderer would fix both -- the honest next step. + * ROTATION is not modelled (the SDF combinators are axis-aligned), so "diagonal"/"leaning" is faked by an offset + + stretch, not a true tilt. + * Layout from relations is heuristic (fixed offsets), not a constraint solve. + +## Synonym grounding + single-pass material-id renderer + gen-stack audit (SEMANTIC-2, +7 tests, 2041->2048) + +Two requested follow-ons to the semantic layer, plus an honest audit of the text-gen and image-gen subsystems. + +### #1 Corpus-relatedness synonyms (the dormant text module put to work) +holographic_semantic gained a SynonymResolver so out-of-vocabulary words resolve to known vocabulary. Two paths, +honestly separated: + * TABLE (default, reliable): a curated synonym table (SYNONYM_TABLE, from SYNONYM_SEEDS) -- deterministic. This is + what actually resolves crimson->red, spherical->sphere, giant->big, chrome->metal in practice. + * LEARNED (opt-in): nearest-known-by-distributional-similarity using learn_word_vectors (random indexing). MEASURED + KEPT NEGATIVE: on a TINY synthetic corpus this is weak/noisy -- ~2/14 argmax ranking, cosines ~0.05. Random + indexing needs a substantial REAL corpus (varied repeated co-occurrence) before it beats the table, as + holographic_text's own demo_text shows on Gutenberg/Brown. So the table leads; the learned path extends coverage + when real text is available. parse_description(text, resolver=) folds synonyms in; a resolved SHAPE adjective + that precedes a head shape noun ('spherical ball') is skipped so it does not spawn a second object. + +### #2 Single-pass material-id renderer (inter-object shadows + see-through glass) +render_scene was rewritten from a per-object z-composite to a SINGLE pass over the UNION SDF (_UnionSDF: min over +objects, with ids() = argmin for the per-pixel material id). Because one march sees the whole scene, objects now cast +soft shadows and ambient occlusion on EACH OTHER (the composite could not). Per-pixel colour/material come from the +material-id buffer; reflective materials pick up a sky reflection; GLASS is see-through via a secondary ray that +continues past the glass surface through the non-glass union, so an object behind glass shows through (tinted, with a +Fresnel rim). MEASURED: a red ball behind a glass panel shows red THROUGH the glass (centre patch R=0.15 dominates +G,B). KEPT NEGATIVES (loud): glass see-through is a single straight-through layer (no refractive bending / Fresnel +transmission); reflections sample only the sky dome (no inter-object mirror reflections); a small object fully +enclosed in a large glass box is still a hard read (the demo scene). The 'inside' nest scale was bumped 0.42->0.55 so +nested objects are more visible. + +### Generation-stack audit (text-gen + image-gen + call sites) -- full doc: holostuff_genstack_audit.md +HONEST verdicts, no force-fitting: + * TEXT GEN (HolographicNGram char n-gram + nucleus decode): the recent geometry/render work does NOT apply, and + dense-Hopfield cleanup snaps to one atom so it cannot replace a full next-char DISTRIBUTION readback; calibrated + back-off was speculative and did not measure as a win, so it was NOT shipped. The text/learning stack's genuine + recent upgrade IS the semantic word-vector grounding (#1). + * IMAGE GEN (make_scene / morph_images / hopfield diffusion): the post-fx pipeline already polishes any generated + or morphed frame through the existing post_process faculty (MEASURED on a make_scene image and a DCT morph + frame) -- the polish 'upgrade' was already available. One convenience wired: morph_scene gained an optional + post= (generate-and-polish per frame, backward-compatible). The genuinely NEW image-gen capability is the + semantic text->3-D render (pixels from a sentence). + * CALL-SITE SWEEP: absorb (HolographicNGram), generate_words (ContextGenerator), _scene/render_scene(tag_list) + (make_scene), morph_scene (morph_images). None broken or improvable by the geometry work; morph_scene gained + post=. + +## Render quality: SSAA + ground plane + dither fix the grainy/low-res look (2048->2050) + +Moose flagged that rendered output looked grainy/low-resolution and "not post-processed." DIAGNOSIS (the honest +root cause): the grain was ALIASING -- render_scene cast ONE ray per pixel, so silhouettes stair-stepped and the +few-sample AO/soft-shadows speckled, and NO post-fx removes that. Bloom/tonemap/vignette are not denoisers; the +postfx denoise() just blurs and supersample()/resample() only upscale an already-undersampled frame. The real fix is +averaging more rays per output pixel. + +Three additive, backward-compatible knobs on render_scene: + * ss (default 2): SUPERSAMPLED ANTI-ALIASING. Render at ss x the target resolution and box-average ss x ss blocks + (postfx.supersample). ss=2 => 4 rays/output pixel, ss=3 => 9. MEASURED: ss=3 cuts whole-frame edge-grain ~11% on + a single sphere silhouette (more at the actual edges); the before/after is night-and-day (render_before.png vs + render_after.png). Cost is ss^2 more rays -- a knob, not always-max. + * ground (default True): a matte _PlaneSDF placed just beneath the lowest object, IN the union, so objects stop + floating and CATCH each other's soft shadows on the floor -- most of what makes a render read as real. (The union + soft_shadow already does inter-object shadows; the ground gives them somewhere to land.) + * dither (default 0.004): tiny sub-LSB noise to break 8-bit gradient BANDING in the sky -- removes the stepped- + gradient artifact without a visible grain (this is dithering, the opposite of film_grain). + +A good post chain for these scenes: denoise(0.6) -> bloom -> aces -> vignette -> gamma. KEPT NEGATIVE: SSAA is brute +force (ss^2 rays); a real renderer would adaptively sample only edge pixels -- not done. Tests: ss>1 measurably +reduces edge grain; ground=True changes the lower frame. + +## Edge-adaptive supersampling + incremental dirty re-render (RENDER-OPT, +2 tests, 2050->2052) + +Moose: "we probably already have trickery for adaptively supersampling only edge pixels; the compile pattern can +speed raytracing; we aren't using the L1-L4/RAM cache stack; only re-render what changed." PROBE-FIRST confirmed all +four exist as principles. Two became real renderer wins; two assessed honestly. Full doc: +holostuff_render_optimization.md. + +* ADAPTIVE SUPERSAMPLING (built, 5x). The principle was already in holographic_adaptive_cache.adaptive_anchors + (sample density ~ |curvature|^power -- crowd where the field bends). The 2-D image analog of "bends" is EDGES + (material-id / silhouette / depth / luminance discontinuities). render_scene(adaptive=True, now default) renders + the base grid once, builds an edge mask (_edge_mask), and supersamples ss^2 rays ONLY on edge pixels. Refactor: + _scene_setup (immutable scene ctx) + _shade_rays (shade an ARBITRARY ray batch) enable tracing edge subrays in + isolation. MEASURED 300x300 ss=3: brute 810k rays/33.8s vs adaptive 157k rays/6.4s (8.4% edges) -- 5.1x fewer + rays, 5.3x faster, mean pixel diff 0.00007 (visually identical). KEPT NEGATIVE: adaptive refines edges found in + the BASE pass, so a sub-base-pixel feature can be missed -- raise base res if it bites. + +* INCREMENTAL DIRTY RE-RENDER (built). SceneRenderer caches the frame + the material-id buffer; set_attr(obj,field, + value) re-shades ONLY that object's directly-visible pixels (idbuf==obj), no re-tracing (geometry unchanged on a + material edit). MEASURED: a material->mirror edit re-rendered 3890/57600 px (6.8%) in 0.1s vs ~3.9s full (~38x), + pixels outside the object byte-identical. The scenedelta content-hash dedup + the id buffer are the mechanism. + KEPT SCOPE: updates directly-visible pixels exactly; appearance through glass / in reflections not incrementally + refreshed (call render()); a geometry/position edit falls back to full render (id buffer stale). + +* CACHE HIERARCHY (already exists). holographic_anim.FrameCache is the hot/warm/cold tier (full-in-RAM L1/L2 analog; + compact deltas-vs-base warm/L3; recompute-from-base cold) -- the TEMPORAL cache across animation frames, explicitly + the honest "L1-L4 analogy, Python cannot touch real CPU caches." SceneRenderer is its SPATIAL analog for one frame + under editing. Honest: analogies (full/fast vs compact/derived), not literal cache-line control. + +* COMPILE-THE-MARCH (assessed, not built). render_analytic already compiles a SINGLE analytic SDF to njit via + codegen+cache. But it renders ONE expr with ONE base colour -- it drops exactly the scene renderer's value + (material-id buffer, see-through glass, ground, dirty cache). Lowering a multi-material scene to one compiled union + that ALSO returns an id buffer + runs the glass pass is a real but non-trivial NEXT build (and the SWEEP negative + stands: deeply nested compound SDFs slow to njit-COMPILE). Logged, not half-done; adaptive AA already removed ~5x. + +## Hyperrealism: path-traced PBR, volumetrics, refractive glass, adaptive sampling (HYPERREAL, +9 tests, 2052->2061) + +Moose: want more rendering features + material properties for regular AND volumetric objects; hyperrealism is fine to +be slow; treat slowness as the signal to find optimization/caching/novel approaches; auto-tune + the holo agent can +choose per frame. PROBE-FIRST found the hyperrealism ENGINES already existed (holographic_brdf full Cook-Torrance/GGX, +holographic_pathtrace Monte-Carlo path tracer w/ emissive, holographic_render.volume_render participating media, +refract_dir + subsurface) -- the semantic renderer just ignored them. The work was WIRING + two real additions. +Full docs: holostuff_hyperreal_materials.md, holostuff_render_optimization.md. + +* PBR PATH-TRACED RENDER (render_scene_pbr). Routes a described scene through the path tracer with a per-object + material(P) callback: nearest-object id -> (albedo, metallic, roughness, emission[, ior]) from PBR_PARAMS. True + multi-bounce GI, GGX highlights, colour bleeding, EMISSIVE objects that light the scene (measured: a glowing blue + ball bleeds blue onto the floor). Faculty render_scene_description(quality='hyperreal', spp, adaptive_spp). ACES + tonemap. ~57s at 200x200 spp=32 -- offline by design (brain side). + +* MATERIAL GROUNDING EXTENDED. PBR_PARAMS (metallic,roughness,emission) + METAL_TINT for gold/copper/steel/mirror. + New material words: gold/brass, copper/bronze, plastic/rubber/vinyl, ceramic/porcelain, emissive/glowing/neon/ + luminous/lamp. So gold is a tinted GGX metal != chrome, plastic a glossy dielectric != matte. + +* VOLUMETRIC OBJECTS (fog/smoke/fire). New volumetric materials parse as their OWN blob objects (no shape noun -- + "a fire", "a smoke cloud"); volumetric_field is a turbulence-modulated soft sphere; render_scene composites them + over the surface frame via volume_render (alpha-over). _VOLUMETRIC={fog,smoke,fire}. Env-clause detection refined + so 'cloud' next to a shape no longer misroutes the clause to environment; volumetric word with no shape still makes + an object. KEPT NEGATIVE: alpha-over composite is NOT depth-aware (a volume + surface that interpenetrate won't + sort); volume is single-scatter absorption/emission, not lit by scene emitters. + +* REFRACTIVE GLASS (path tracer). fresnel_dielectric (Schlick, R0=0.04 for glass) + a dielectric branch in path_trace: + per ray, REFLECT with Fresnel prob else REFRACT (refract_dir/Snell, TIR handled). The refracted ray can't be marched + by sphere_trace (it treats the interior as an immediate hit), so _march_through steps the ray by |sdf| THROUGH the + glass to the far face, then refracts OUT -> two-interface bending, see-through with a Fresnel rim (measured: a glass + ball refracts the floor through it). _pbr_props gives glass ior=1.5; material callback returns a 5-tuple; path_trace + _unpack_mat accepts 4- or 5-tuple (backward compatible). KEPT NEGATIVES: smooth glass only (no rough/frosted, no + dispersion); transmission albedo-tinted, not true Beer-Lambert over path length. + +* ADAPTIVE MONTE-CARLO SAMPLING (the "slowness as signal" optimization). path_trace gained return_variance (per-pixel + variance-of-the-mean map) and active (a pixel mask -- trace only those). render_scene_pbr(adaptive_spp>0): render + spp everywhere + variance, then spend adaptive_spp EXTRA samples only on the noisiest pixels (above noise_pct + percentile), sample-count-weighted combine. The sample-domain analog of the edge-adaptive AA (spend where it bends + / where it's noisy). MEASURED 160x160: uniform spp=64 65.7s vs adaptive 16+48 on noisiest 30% 35.4s -- 2.11x fewer + samples, 1.86x faster, PSNR 32.1 dB vs the uniform-64 reference (near-identical). Hook for the holo agent + (decide_confidence) + auto-tuners to set the budget per frame. + +* DENOISE OPTIMIZATION (measured). Low-spp + the engine's denoise: spp=8 + denoise vs spp=96 -- 12.3x faster, +2.65 dB + recovered by denoise (the OptiX/OIDN "render fewer samples, clean up" trick). FFT denoise over-smooths; albedo/ + normal-guided NLM is the honest upgrade. + +CROSS-ARC: probe-first (engines already existed; gap was wiring); measure with negatives loud; the adaptive principle +(sample where the field bends / where variance is high) recurs in BOTH the pixel domain (edge AA) and the sample +domain (adaptive spp); glass interior transport needs |sdf| marching since sphere_trace can't go inside. + +## Holographic volumetrics: the closed-form line integral over an FPE density field (VOLINT, +5 tests, 2061->2066) + +Moose's push: we aren't using the holographic SPACE -- traditional renderers can't represent 4d let alone 4096d, and +they have NO model of empty space (they discover it by marching). The whole density field, occupied AND empty, should +be a property of one hypervector; fog/atmosphere should then be real-time "if done properly". PROBE-FIRST: the FPE +substrate already existed -- holographic_fpe.VectorFunctionEncoder (N-d Fractional Power Encoding / VFA) and +holographic_fpefield.HolographicField (an SDF carried as ONE hypervector, edit=bind, union=bundle). The missing piece +was rendering FROM it. Full doc: holostuff_holographic_volumetrics.md. + +* THE MECHANISM (new module holographic_volint.py). A density field is F = sum_i w_i encode(p_i) -- one hypervector; + density(x) ~ is the FPE Bochner/RBF kernel-density read. Because the FPE basis is a PHASE code, + encode(O+tD)_j = exp(i O.Theta_j) * exp(i t D.Theta_j), and the integral of a complex exponential is closed form. So + the LINE INTEGRAL of density along a ray is: + integral_0^L density(O+tD) dt = Re sum_j F_spec_j * exp(-i O.Theta_j) * (1 - exp(-i (D.Theta_j) L))/(i D.Theta_j) + -- ONE inner product per ray, NO marching, vectorised over all rays (chunked so the (chunk,dim) complex temporaries + stay small). Theta_k = scale_k * phases_k from each axis ScalarEncoder; F_spec = fft(F). + +* MEASURED. Closed-form vs a 160-step marched reference: correlation 1.0000, mean rel err 0.000 -- EXACT. At image + scale (200x200 = 40k rays, dim=2048): closed-form 8.8s vs marching-the-same-field ~800s (extrapolated) -- ~90x + faster (= ~steps-fold; closed form is O(R*dim) once vs O(R*steps*dim) marching the holographic field). EMPTY SPACE + reads tau~0 with NO marching -- a property of the field, the thing a marcher cannot have at the start of a render. + +* RENDERER. render_fog composites atmospheric fog over a frame using closed-form optical depth per camera ray (eye-> + hit via the depth buffer; render_scene now exports stats['depth']): T=exp(-density_scale*tau), out=bg*T+fog*(1-T). + Distant objects fade into fog with no per-ray volume march (_fog_holographic.png). Faculty: + UnifiedMind.holographic_fog_volume(centers, weights, bounds, dim, bandwidth) -> HolographicVolume. + +* CALIBRATION. The raw spectral integral is in FFT units (~1e6); __init__ does a one-time cheap marched calibration + (6 probe rays x 24 steps) to fold the FFT/norm constant into self._cal so optical_depth is in integrated-density + units. The closed-form-vs-marched check found the same single global scale; baked in. + +* KEPT NEGATIVES (loud). (1) This is the OPTICAL DEPTH / extinction (absorption + atmosphere), exact and fast. The + FULL volume-rendering integral weights emission by the running transmittance T(t) INSIDE the integral, which is + nonlinear (exp of a partial integral) and is NOT closed-form this way -- self-shadowing emissive smoke still wants + marching (the documented next step). (2) "Real-time" is honest only in the sense ~steps-fold over marching the same + field and EXACT; the absolute 8.8s at dim=2048 is the per-ray inner-product cost -- true real-time would come from + the GPU muscle layer (this is the NumPy brain), a real-valued reformulation, or smaller dim. (3) density is an RBF + KDE of the bundled samples -> smooth; sharp edges need denser samples / smaller bandwidth. (4) Empty-space-is-known + holds WITHIN the encoder's working bounds (FPE aliases periodically outside). + +CROSS-ARC: this is the most literal "move the render into the holographic space" -- the field is one vector, the +integral is one algebra op, empty space is known up front. The win that's unique to the representation is EXACTNESS + +empty-space-awareness + composability (bundle to add fog, bind to move it), not raw NumPy speed; the speed is the +muscle layer's job. Real published basis: Frady/Kleyko/Sommer VFA "Computing on Functions"; Komer/Eliasmith SSP; Plate. + +## Holographic radiance field + tiling that breaks the capacity wall (RAD, +6 tests, 2066->2072) + +Moose: finish the smooth-radiance/GI field, AND -- the key correction -- capacity is NOT a hard wall. The architecture +overcomes it with DELTAS/DIFFS + DETERMINISTIC spatial data structures that MARK locations in the holographic space + +tiling/chunking + superposition; "we don't build huge full representations, we use deltas and collapse on demand, +composably." The "we know where/what everything is" idea spans lighting, shadows, normals, SSS, refraction, caustics, +reflections, translucency, all physics, voids, gravity lensing, and BIDIRECTIONAL ray<->object lookup. This thread +delivers the radiance instance + the capacity answer. Full doc: holostuff_holographic_radiance.md. + +* RADIANCE AS A FIELD (holographic_radiance.HolographicRadianceField). Carry the colour leaving each point as FPE + bundles: three channels weighted by colour + a COVERAGE field weighted by 1. Query = Nadaraya-Watson ratio + radiance(x) = / -- the kernel-weighted average colour, SELF-NORMALISING (the FFT/ + bundle constants cancel in the ratio, so NO calibration, unlike the density integral). coverage~0 = empty space, + known from the field. NOTE: this query is MORE correct than the engine's cosine-based query, which leaves a spurious + per-field-norm factor that does not cancel in a ratio. + +* FAST SPECTRAL BAKE (no per-point loop, bounded memory). specs[:,c] = sum_i rgb_i^c exp(i p_i.Theta), built as a + CHUNKED MATMUL (cs.T @ rgb4 accumulated over point-chunks). Query = Re(exp(-i x.Theta) @ specs), also chunked. This + is the OOM lesson applied: never build the (N x dim) dense array -- chunk it. ~2.6s to bake 40k points at dim 2048. + +* TILING BREAKS THE CAPACITY WALL (holographic_radiance.TiledRadianceField). The single-vector capacity wall is REAL + (a D-dim vector holds ~D items; 40k pixel-samples in dim 2048 -> ~15.6 dB mush). The engine's HoloOctree already + solves this for occupancy ("tile 3D space so each node's wave stays inside one vector's capacity"); this does the + same for RADIANCE. Space -> deterministic grid of bricks; each occupied brick a small field over its OWN samples (+ + a one-cell halo for border continuity); only occupied bricks stored (sparse dict -- no giant dense structure). A + query routes each point to its brick deterministically. MEASURED on a dense 200x200 frame reconstructed purely by + query: single vector 15.6 dB; tiled grid=8 20.9 dB; grid=14 24.2; grid=20 26.6; grid=28 28.9 -- the wall MOVES with + grid refinement, even at SMALLER per-brick dim (768). Capacity = per-brick capacity x #bricks. + +* DELTA / COMPOSABILITY (rebuild_cells). Bricks are independent, so a change to one region rebuilds ONLY its bricks -- + an O(change) update, not a global redo. MEASURED: recolouring 1 of 327 bricks rebuilt 1 brick and changed 965/40000 + pixels (only the touched region); the rest byte-identical. This is the delta/diff + spatial-marker pattern Moose + pointed at, for radiance. + +* THE TRIO. Geometry (FPEField SDF), density (volint closed-form integral), radiance (this) are all hypervector fields + over all space including empty space -> a render becomes a QUERY of fields. Faculty: holographic_radiance_field. + +* KEPT NEGATIVES (loud). (1) The field STORES radiance; a solver (path tracer) BAKES it first -- real-time is the + query/playback, not the bake (as light fields / PRT / NeRF baking work). (2) Radiance baked from one view's pixels + is VIEW-INDEPENDENT/diffuse-ish; full view dependence is the 5-D plenoptic field (add 2 direction axes + more + samples). (3) RBF KDE -> smooth; hard radiance edges band-limit (denser samples / smaller bandwidth). (4) The + single-vector capacity wall is real and was honestly reported BEFORE tiling moved it -- the negative that pointed at + the fix. + +CROSS-ARC: the capacity "wall" is per-vector; the system's answer is tile + delta + deterministic spatial index + +superposition -- the same pattern across the whole "we know where/what everything is" scope (lighting, shadows, +physics, reflections, voids...). Basis: Frady/Kleyko/Sommer VFA; Komer/Eliasmith SSP; Nadaraya-Watson; Levoy/Hanrahan +light fields; the engine's own HoloOctree (TILE3D) and delta-chain work. + +## Bidirectional ray<->object index: edits as bounded, bit-exact deltas (RAYIDX, +5 tests, 2072->2077) + +Moose: the information solvers usually MARCH/gather is already known once things are exactly queryable -- so keep it and +improve the pipeline across the board. Concretely: record which objects each ray TOUCHED along its path, so an edit +re-shades only the rays that can change -- including through glass / in reflections (the bidirectional ray<->object +lookup). PROBE-FIRST: the incremental SceneRenderer already exists but its docstring ADMITS the gap -- "through glass or +in reflections is not incrementally refreshed". union.ids(P) gives the object at any hit. The new module closes that gap. +Full doc: holostuff_ray_index.md. + +* THE INDEX (holographic_rayindex.RayPathIndex / build_ray_index). Trace the scene geometry the renderer already + traces -- primary sphere_trace + the glass-secondary ray (object seen THROUGH glass) -- and record touched[pixel, + object]=True. pixels_touching(ids) is the reverse map: object -> the exact pixels to re-shade when it changes. + indirect_pixels(obj) = pixels that touch obj only on a SECONDARY ray (the ones a primary-id-only renderer MISSES). + +* BOUNDED, BIT-EXACT DELTA (delta_reshade). On a colour/material/light edit, re-shade ONLY pixels_touching(changed) via + the deterministic _shade_rays and composite into the cached frame. MEASURED (recolour a ball seen through a glass + ball, 220x220): the index found 4508 through-glass pixels; delta re-shaded 9.3% of the frame in 0.08s vs 0.75s full + (~9x), max err 0.00e+00 (BIT-EXACT). The PRIMARY-ID-ONLY incremental path (old SceneRenderer) scored 28.2 dB -- WRONG, + because it misses 4442 of those through-glass pixels. The index catches every one (_idx_indirect.png: the through-glass + region highlighted is exactly where the primary hit is the GLASS, not the ball). + +* THE PRINCIPLE. The trace already discovers where every ray goes; traditionally that's discarded and re-gathered each + frame. Keeping it as a bidirectional index turns any localized edit into an O(touched pixels) update that is correct + on the INDIRECT pixels too. This is the "improve the pipeline across the board" lever -- the same index generalises to + specular-reflection bounces (record the reflected hit) and to spatial bricks (record bricks, for region edits), and + pairs with the radiance/density/SDF fields (query, don't march). + +* FACULTIES: ray_path_index(objects, camera, ...) -> RayPathIndex ; delta_reshade_scene(edited, index, changed_ids, + base_frame, camera) -> (updated_frame, mask). + +* KEPT NEGATIVES (loud). (1) A MATERIAL/colour/light edit leaves geometry unchanged, so the index stays valid and the + delta is exact; a MOVE changes geometry, so the index must be rebuilt for the affected region first (the dirty-region + story) -- the index makes the RE-SHADE cheap, not the geometry re-trace free. (2) Bit-exactness requires matched + sampling: delta_reshade shades 1 ray/pixel, so it is exact against an ss=1 render; an edge-supersampled (ss>1) full + render differs at edges unless the delta supersamples the same edge pixels (a follow-on). (3) Built for the primary + + glass-secondary segments render_scene actually traces; render_scene's mirror term reflects the SKY not objects, so a + mirror-object-reflection demo needs object reflection added to the shader (identical index pattern). + +CROSS-ARC: "the info solvers march for is already known" -> record it once, query it on every edit. Closes the exact gap +the incremental renderer documented (glass/reflection), with a bit-exact, ~9x-bounded delta and the indirect pixels a +primary-only path cannot see. Pairs with the field trio (geometry/density/radiance as queryable hypervectors). + +## Wiring the index across the pipeline: object reflection, region-keyed MOVES, fog in render_scene (+3 tests, 2077->2080) + +Moose: do both follow-ons (brick index for moves, object reflection) AND wire everything to the REAL pipeline, not just +tests -- shadows, reflections, volumetrics, geometry all benefit from "record once, query on every edit". This span did +that, all landing in render_scene / _shade_rays, with the index extended to the new secondary rays. + +* OBJECT REFLECTION IN THE REAL SHADER. _shade_rays previously reflected only the sky dome. Now reflective surfaces + (refl>0.05: mirror 0.85, metal 0.5) cast a ONE-BOUNCE reflection ray traced against the union; a hit object is shaded + (colour x (amb*ao + lambert*shadow*sun)) and blended by refl, else the sky. Wired automatically into render_scene + (every render now reflects objects). build_ray_index records the reflected-ray hit, so an edit to a reflected object + updates the mirror pixels. MEASURED (recolour a ball reflected in a mirror box, 200x200): 764 reflected pixels, delta + re-shade BIT-EXACT (0e+00), 10.7% of frame; a primary-id-only path misses them (_refl_indirect.png highlights them). + +* REGION-KEYED INDEX FOR MOVES (BrickRayIndex). A MOVE changes geometry, so the object index goes stale; key by REGION + instead. The index stores, per ray, the segment (origin, dir, hit-distance) AND the surface hit point + sun direction. + pixels_through_region(aabbs) flags, by EXACT vectorised ray-box (slab) tests: (a) camera rays that REACH the region + (occlusion) and (b) pixels whose SHADOW ray (hit point -> sun) crosses it (cast shadow -- moving an object moves its + shadow, far from the object's own pixels). delta_reshade_move(obj, delta) flags old-AABB u new-AABB (padded for AO), + re-shades only those, bit-exact. MEASURED (move a ball through a 3-object scene, 160x160, four different moves): + re-shaded 22.8-48.7%, covers EVERY changed pixel, max err 0.00e+00 across all moves. + + KEPT NEGATIVES (loud, debugged honestly): (1) The first cut SAMPLED bricks along each ray (discrete points) -> it + could skip a thin brick and UNDER-cover; replaced with the exact ray-box slab test (no sampling, conservative). (2) + Occlusion alone missed the cast SHADOW (constant ~0.75 error on ground pixels far from the object) -> added the + shadow-ray box test. (3) Then background pixels the RAISED ball moved into were still missed: snap_to_bricks CLAMPED + the region to the original (old-scene) grid bounds, shrinking it when the object left them -> fixed to align to the + unbounded brick lattice without clamping. After all three, bit-exact. (4) Soft-shadow PENUMBRA / AO halo are bounded + by the AABB pad (ao_pad, default 0.6) -- a very wide penumbra would need a larger pad; the move-delta re-shades a + larger fraction than a material edit because it must cover old+new occlusion AND old+new shadow. (5) Reflections/GI of + a MOVED object (its reflection in a distant mirror) are not yet in the move set -- same pattern (test the reflected + segment), noted for the next pass. + +* FOG IN THE REAL RENDER PATH. render_scene gained fog=/fog_density/fog_color/fog_max_dist: if fog is a + HolographicVolume, the closed-form volint optical depth is composited along each camera ray using the depth buffer, + before post/dither. So the holographic (no-marching) fog is now a first-class render_scene option, not a separate call. + +* FACULTIES: ray_path_index (records primary + glass + reflection); delta_reshade_scene (material/colour/light edit); + brick_ray_index + delta_reshade_move (geometry MOVE, caches the scene ctx). All return real frames from the real shader. + +CROSS-ARC: the index now spans the secondary rays the renderer actually casts -- through-glass, reflection, and shadow -- +so material edits, light edits, AND moves all become bounded, bit-exact deltas keyed off information the trace already +discovered. This is the "everything benefits" lever wired into render_scene/_shade_rays, not a side demo. + +## Reflection/GI of a MOVED object: the secondary-ray move case (+1 test, 2080->2081) + +Moose: do the flagged next step -- a moved object's image in a mirror or through glass must update too. Same pattern as +the shadow ray: record the SECONDARY segments the shader already casts and test them against the moved region. + +* WHAT WAS ADDED. build_brick_index now also records, per pixel, the REFLECTION ray (off mirror/metal) and the GLASS + see-through ray -- their origin, direction, current hit-distance, AND the object id each currently hits (sec_id). + BrickRayIndex.pixels_for_move(obj, old, new) returns the bounded move set: occlusion (camera ray reaches old/new) U + cast shadow (shadow ray crosses old/new) U secondary (sec_id==obj -> it left the reflection/glass; OR the secondary + ray now reaches the new region -> it moved into view). delta_reshade_move uses pixels_for_move. + +* MEASURED. GLASS move (lift a ball seen through a glass ball, 170x170): bit-exact (0e+00), covers every changed pixel, + re-shaded 89.8% -- and WITHOUT the secondary test it would MISS 3729 through-glass pixels, so the secondary test is + REQUIRED and now correct. MIRROR move (mirror ball reflecting a moved ball): bit-exact, 67.3%. + +* HONEST NEGATIVES (loud). (1) Through-glass / in-mirror MOVES re-shade a LARGE fraction (67-90%) because secondary + rays sweep a wide solid angle -- a moved object behind a big glass ball or in a big mirror genuinely affects a large + region; the index is conservative-correct, but the saving shrinks as the refractor/reflector grows. (Material/colour + edits stay cheap; it is the geometry MOVE through a big secondary surface that is costly.) (2) The first cut tied the + secondary segment's reach to its OLD hit distance and left a 47-pixel silhouette fringe (not bit-exact); fixed by + recording sec_id and splitting into "currently shows it" (sec_id==obj) + "moves into it" (ray reaches new region at + full reach). (3) In some geometries the shadow ray already covers the reflection pixels (mirror case missed 0 without + the secondary test) -- the secondary test is what makes it correct REGARDLESS of geometry, not a per-scene luck. + +CROSS-ARC: the index now covers EVERY secondary ray the shader casts -- primary, shadow, reflection, glass-see-through -- +so colour/material/light edits AND geometry moves are bounded, bit-exact deltas, with the honest cost reported where a +wide secondary surface makes the move set large. + +## SSS + translucency wired into the shader and the index (+2 tests, 2081->2083) + +Moose: bring subsurface scattering, translucency, and the rest of the list onto the record-once/query-on-edit approach +where it fits. PROBE-FIRST paid off again: `subsurface()` (Beer-Lambert interior march toward the light, thin->glow) +already existed in holographic_raymarch but was NEVER wired into the real shader `_shade_rays`; there were no SSS or +translucent material names. So the work was wiring, not building. + +* MATERIALS. Added words: wax/jade/marble/skin/candle -> "wax" (SSS); translucent/frosted/milky -> "translucent" + (diffuse see-through). MATERIAL_RENDER gained sss/translucent flags; ctx gained is_sss, is_translucent. + +* SHADER (real pipeline). _shade_rays now: (a) for SSS pixels, adds a forward-scatter GLOW = colour * subsurface(union, + P, N, sun_dir) * sun_i -- thin parts transmit the sun and glow (wax look); (b) for translucent pixels, a diffuse + tinted see-through (like glass but blurred, no refraction) showing the object behind. render_scene gets both for free. + +* INDEX. SSS is a SURFACE term on the object's OWN pixels, so the object index (material edit) and brick occlusion + (move) already cover it -- the delta re-shade recomputes the SSS via _shade_rays. Translucency is a SEE-THROUGH + secondary ray, so build_ray_index and build_brick_index now treat see_through = is_glass | is_translucent: a frosted + object gets the same glass-style secondary recording, so an object BEHIND it updates via the index (edit or move). + +* MEASURED (bit-exact, deterministic). SSS material edit (recolour a wax ball): bit-exact, re-shades only the ball's + own pixels. SSS MOVE: re-shaded 27.4%, covers-all, bit-exact -- SSS recomputed at the new position. Translucent move + (lift a ball seen through a frosted ball, 2104 see-through pixels): bit-exact, covers-all, re-shaded ~91% (the same + wide-secondary honest cost as glass). All renders unchanged for non-SSS/non-translucent objects (no regression). + +* KEPT NEGATIVES. (1) SSS here is the Beer-Lambert thin-glow approximation (interior path toward the light), not full + multiple-scattering diffusion -- the field-native term, honest about being an approximation. (2) Translucency reuses + the single-layer see-through (no scatter blur kernel, no refraction) -- a diffuse tint, not a true rough-dielectric + BTDF. (3) Through-translucent/glass MOVES re-shade a large fraction (wide secondary solid angle) -- conservative and + bit-exact, but the saving shrinks as the see-through surface grows (same cost noted for glass/mirror moves). + +REST-OF-LIST MAP (honest, where the approach fits): shadows DONE (shadow ray in the move set); reflections DONE; +glass/refraction-see-through DONE; SSS + translucency DONE (this entry). NATURAL NEXT, same pattern: bump/normal/ +displace and face-normals are pure SURFACE-shading edits -> the OBJECT index already covers them once they vary a +material (re-shade the object's pixels); caustics + GI bounce are extra SECONDARY segments (record the bounce/caustic +ray, test vs the moved region) -- the reflection/shadow pattern one level deeper. GENUINELY DIFFERENT (not a quick wire, +flagged honestly): fluid/smoke/particle/rigid/soft-body PHYSICS and collisions change geometry every frame (the index +must be rebuilt per step -- the delta helps the SHADE, not the sim); voids are just empty regions (already free via +empty-space SDF/field queries); gravitational lensing bends the primary ray itself (the ray segments become curves -- +the slab tests would need arc/segment approximations). The lever applies cleanly to shading and discrete edits; it does +not make a per-frame physics sim free, and that boundary is stated rather than papered over. + +## The missing piece: a render SESSION so unchanged re-renders are FREE and edits stream as deltas (+1 test, 2083->2084) + +Moose reported real slowness re-rendering an UNCHANGED scene (same camera, no edits) and expected the bidirectional +index to make that ~free / realtime, especially for pixel streaming. PROBE-FIRST found the gap: the existing +`SceneRenderer` cache (a) always calls the full render_scene on render(), with NO "nothing changed -> return cached" +path; (b) uses the OLD primary-only idbuf incremental, not the new bidirectional index (so glass/reflection/SSS edits +and moves weren't covered); (c) has no delta pixel-STREAM. And render_scene defaults to ss=2 (4x supersample) -- wrong +for streaming. So calling render_scene each frame re-traced + re-shaded the whole frame every time, even with no changes. + +* NEW `IncrementalRenderer` (holographic_rayindex) -- a render SESSION on the bidirectional index: + - `render(objects)` -> (frame, mask). If the scene KEY (per-object shape/colour/material/size) is unchanged, returns + the CACHED frame with an empty mask -- ZERO work. Else full render (ss=1 default, delta-exact) + build ray & brick + indices, cache. + - `edit(obj, field, value)` -> re-shade only pixels_touching(obj) via the ray index (through-glass / reflection / SSS + / translucency all covered); bit-exact, bounded. + - `move(obj, delta)` -> brick-index delta (occlusion + shadow + secondary); bit-exact; indices rebuilt (geometry + changed). + - `stream_delta(mask)` -> (ys, xs, rgb) of ONLY the changed pixels -- the wire payload is O(changed), not O(frame). + +* MEASURED (256x256, 2-object scene, ss=1): first render 3.57s (full, unavoidable -- the authoritative frame); SAME + scene again 0.00014s (FREE, 0 px); colour edit 0.038s, 1653 px = 2.5% of frame, BIT-EXACT vs a full re-render, stream + payload 1653 px vs 65536 = ~40x less data, ~61x faster than a full render_scene(ss=2). The unchanged case went from a + full re-trace to nothing. + +* GUIDANCE (the actual fix for the reported slowness): use `mind.incremental_renderer(cam, w, h)` for repeated + rendering / live editing / streaming instead of calling render_scene every frame; render_scene is a ONE-SHOT. Use + ss=1 for the stream (ss=2 is ~1.5-2x slower, for a final still only). Export the .glb ONCE (or only after a move) -- + colour/material/light edits and unchanged frames don't change geometry, so the glb is stable; stream pixel deltas per + frame. + +* KEPT NEGATIVES. (1) The FIRST frame is inherently full-cost -- the brain does one authoritative render (per-pixel AO + + soft-shadow marches dominate); the session's win is not REPEATING it, and the GPU muscle layer is for absolute + first-frame speed. (2) A camera MOVE invalidates every ray -> full re-render (the index is camera-space; reprojection + is a future item). (3) Edits are ss=1 delta-exact; a final ss=2 still is a separate full render. + +* SEPARATE BUG FLAGGED (not the session): the parser's CHAINED "A beside B beside C" layout puts objects 0 and 1 at the + SAME centre (they overlap) -- 2-object "beside" is fine, 3+ collides. Surfaced while testing; worth a realizer fix. + +CROSS-ARC: the index made per-edit work bounded; the SESSION is what turns that into "pay only for what changed" ACROSS +FRAMES -- unchanged = free, edit/move = a small delta stream. That is the realtime-streaming path the index was built to +enable, now wired end to end. + +## Camera move != full re-trace: temporal REPROJECTION (the 3DGS / DLSS / V-Ray-realtime idea) (+1 test, 2084->2085) + +Moose (correct pushback): a camera move should NOT invalidate everything -- 3D Gaussian splats, V-Ray/Redshift realtime, +and Intel's OIDN+XeSS all reuse the previous frame under a moved camera. The reason: DIFFUSE shade (Lambert + AO + soft +shadow) is VIEW-INDEPENDENT -- a world point's colour doesn't change with the camera, only WHICH PIXEL it lands on. Only +reflections / glass / specular are view-dependent, plus newly-disoccluded pixels. + +* NEW IncrementalRenderer.reproject(new_camera). Uses the per-pixel WORLD hit point the index already stores (a + G-buffer: _P, _hit, _viewdep). Forward-project every cached hit into the new view (_project_points = the exact inverse + of Camera.ray_dirs), z-buffer so the nearest wins, and re-shade ONLY (a) holes -- disocclusions + sky (sky is + view-dependent but cheap), and (b) view-dependent pixels (refl>0.05 | glass | translucent). Reused diffuse pixels + carry over without re-tracing the expensive AO/soft-shadow marches. Camera-space indices are marked stale and rebuilt + LAZILY (_ensure_fresh) only when a later edit/move needs them -- pure navigation pays nothing extra. + +* MEASURED (200x200, orbit 5 deg, ss=1): FRAME-FILLING scene (11% sky) -> reproject 0.17s vs full 1.24s = 7.2x, only + 17.8% re-shaded, PSNR 32.7 dB. SPARSE scene (32% sky) -> 3.5x, 38% re-shaded (mostly cheap sky), 31.4 dB. The win + GROWS with how much reusable geometry fills the frame; re-shaded (hole + view-dependent) pixels are EXACT. + +* KEPT NEGATIVES (loud, the honest muscle-layer trade every realtime renderer makes). (1) Reprojection is APPROXIMATE, + not bit-exact: round-to-nearest resampling shifts colours up to half a pixel, so PSNR ~31-33 dB (edges/silhouettes + carry the error; smooth diffuse regions are near-exact). TAA/DLSS/XeSS accumulate sub-pixel jitter over frames to fix + this -- a future item; for now call render() for a bit-exact still. (2) A 2x2 splat to close sub-pixel gaps was tried + and made it WORSE (blur, 27 dB) -- reverted to nearest; the "holes" are mostly SKY (no world point, genuinely + view-dependent) not sub-pixel gaps, and sky re-shades cheaply. (3) Fast-moving / large camera jumps disocclude more -> + more re-shade -> less win (bounded by a full render at the limit). (4) view-dependent = whole object flagged + (reflective/glass/frosted) -> those objects' pixels always re-shade on a camera move (correct, since their look + changes with view). + +CROSS-ARC: this closes the last "camera move is a full render" gap -- the same record-once idea (the trace's world hit +points) now serves camera MOTION too, reused where the shade is view-independent and re-shaded only where it genuinely +changes. Editing, moving, AND navigating are all now "pay for what changed." + +## The composable substrate: labelled REGION FIELDS -- a boundary says how to regard what's inside (+5 tests, 2085->2090) + +Moose's unification: treat anything as mesh/particle/smoke/fluid/light -- it's all vectors being transformed and +accumulating data by collapsing higher dimensions; the missing piece is composing a multi-body system by specifying a +3D boundary that defines how the information inside it is REGARDED, with combined processing and precise culling. +PROBE-FIRST: attribute_field/sample_attribute (per-point value fields) and the SDF DSL (union/intersect/subtract/ONION +shells) already exist; the missing primitive was the LABELLED-REGION algebra that ties a boundary to an interpretation. + +* NEW holographic_regionfield. `Region(sdf, label, priority, material, behavior, data)` = a boundary (SDF, negative + inside) + how to regard its interior. `RegionField.classify(points)` -> per-point winning region by priority (-1 = + empty), vectorised, O(regions). That one classification drives THREE things from ONE field: + - MATERIAL: `material_at(points)` -> rgb by region; and because regions LAYER by priority, `slice(origin,u,v)` cuts + the volume open and returns the material image -> SLICE IT OPEN, SEE THE LAYERS (no special case). + - BEHAVIOUR: the label can be 'cloth'/'fire'/'smoke'/'fluid' instead of a colour, so `behavior_at(points)` picks the + SIMULATION per point -- cloth-on-fire-to-smoke is three region labels over one field with moving boundaries, not + three bolted-on engines. + - CULLING: `cull(points)` = classify>=0; a point outside every region is KNOWN-empty (the SDFs say so up front) and + skipped with no marching -- the same "empty space known, not discovered" property the density/radiance fields have. + +* MEASURED. Layered planet (atmosphere/crust/ocean-band/mantle/offset-core): the cut shows 5 distinct materials + (_region_slice.png -- concentric rings, core off-centre). Culling 216,000 grid points in 0.076s: 84.7% known-empty + and skipped, 15.3% inside a region. Behaviour-by-region: one classify returns ['cloth','fire','smoke',None] over a + stacked scene -- one field picking the sim. + +* HONEST SCOPE (loud). This is the composable SUBSTRATE -- the labelled-region algebra (classify/slice/cull/material/ + behaviour). It is NOT the simulations: a real cloth solver, a combustion model, a fluid step, fractal biome + generation, instanced grass, auto-LOD are the APPLICATION layer that READS a region's behaviour label and runs. What + the substrate earns is that they compose over ONE field with ONE classification and free precise culling, instead of + each being a siloed pipeline -- which is exactly Moose's point ("it's all mashed together at the superposition + anyway"). The hard, genuinely-unbuilt parts are the solvers and the time-evolution of the boundaries (a region + shrinking as cloth burns, fragments detaching when disconnected) -- flagged as real work, not claimed. + +REST-OF-VISION MAP: material-by-boundary + slice-layers DONE; behaviour-by-boundary (sim selection) DONE as the +label; precise culling DONE (free from the SDFs). NEXT, same primitive: drive the RENDERER's material from +material_at(hit_points) (a biome planet shaded by region); connectivity/fragment detection (label a region, flood-fill +its inside-mask, split when a component disconnects -- the cloth-falls-off case); time-varying boundaries (region SDFs +that move/shrink per step). GENUINELY SEPARATE (solvers, not this substrate): the cloth/fire/fluid numerics and LOD -- +they plug into the behaviour label but are their own work. + +## Region material in the real shader (biome planet) + coherent secondary rays (bounce = transform) (+4 tests, 2090->2094) + +Two threads from Moose's ray/region vision, both wired to the real pipeline and measured. + +### (a) Region material drives the shader -- a biome planet from ONE sphere +The region field now feeds the actual renderer. `render_scene(..., region_field=rf)` sets `ctx['region_field']`, and +`_shade_rays` takes each hit point's albedo from `rf.material_at(P_hit)` where a region covers it (falling back to the +object colour elsewhere). MEASURED: one plain grey sphere renders as a biome planet -- ocean base, green/forest +continents, a desert strip, white ice cap -- material entirely from region membership at the hit point +(_planet_biome.png). No per-object colours, no texture map: the boundary says what the surface IS. This is the planned +"drive the renderer from material_at(hit_points)" step, done. (Reflection-of-region and SSS-of-region still read the +object colour -- a small honest follow-on.) + +### (b) Coherent secondary rays -- a bounce is a TRANSFORM of its parent (holographic_raycoherence) +Moose's reframe of the flagged secondary-ray weakness: a bounce ray is not new work, it's a transform of the parent +(origin -> hit, direction -> reflect about N, bounce += 1 -- the only new information). `reflect_transform` is that +transform; N bounces are N applications. And neighbouring reflection rays off a SMOOTH surface are coherent, so trace +a SPARSE stride-grid of reflective pixels and reconstruct the perpendicular neighbours by gated bilinear interpolation +(continuity gate: same object id + aligned normal so we never blend across the reflector's own edge), with an +exact-trace FALLBACK where the reconstruction is uncertain. `coherent_reflection` returns (reflected, n_traced, +n_mirror). + +* MEASURED (huge mirror ball, smooth reflection = sky/ground): stride 4 traces **22% of reflection rays** and + reconstructs the rest at reflection PSNR ~28 dB (MSE 1.6e-3) -- a 4.6x cut in secondary-ray work. On a 200^2 frame, + ~2.1x wall-clock on the reflection pass. + +* KEPT NEGATIVE (loud, and instructive). With a sharp reflected-CONTENT edge (a red box visible in the mirror), + reflection PSNR caps at ~20 dB and tightening `var_tol` (0.06 -> 0.004) barely moves it (19.7 -> 20.8 dB) while + spending more rays. WHY: the continuity gate sees the REFLECTOR's geometry (a smooth ball -> everywhere "coherent"), + but the reflected IMAGE has an edge the geometry doesn't predict; the only thing catching it is the coarse 4-corner + colour variance, which a thin high-contrast edge slips through. So the method is a clean win on coherent reflection + CONTENT (sky, ground, gradients) and blurs sharp content edges. The honest next step is a reflected-content edge + detector (refine where neighbouring SAMPLES disagree, not just corners) -- a second pass, deferred. High-curvature + reflectors (a small mirror ball) also reduce the win: reflection changes fast, so more fallback (58% traced on the + tiny ball) -- correct behaviour, smaller saving. + +CROSS-ARC: this is the VSA thesis on the render side -- a secondary ray is `bind(parent, reflect_op)` with a depth +increment, and the reflected field is reconstructed by the same kernel/Nadaraya-Watson interpolation the radiance +field uses. The coherence we exploit is exactly why the ray G-buffer (record once, reuse) pays: adjacent rays are +diffs of each other. + +## Ray differential FRAMES: a perpendicular pencil transported through a bounce reconstructs the bundle (+5 tests, 2094->2099) + +Moose's idea, understood correctly this time (the earlier build was the WRONG thing -- screen-space interpolation of +finished reflections; this is the RIGHT thing): a ray carries a small local frame of PERPENDICULAR rays (centre + 4 +marginal, offset +u/-u/+v/-v). The frame is the same LOCALLY but, when the centre ray bounces off a surface, the +marginal rays hit slightly different points with slightly different normals, so the reflected pencil CONVERGES or +DIVERGES -- reoriented globally. That convergence/divergence IS the physics: flat -> parallel (sharp mirror), convex +-> spread (blurred), concave -> focus (a CAUSTIC), + a base spread from roughness / soft-light size (a glossy lobe / +penumbra), + a per-wavelength split (DISPERSION). Each ray carries a Gaussian (the pencil cross-section / a +covariance), transported through interactions -- so ~5 rays reconstruct a bundle Monte Carlo would sample with +hundreds. Published lineage named for grounding: ray differentials (Igehy 1999), cone/beam tracing (Amanatides 1984, +Heckbert 1984), covariance tracing (Belcour et al. 2013). NEW holographic_raydiff. + +* MEASURED. (1) The 5-ray frame PREDICTS the 100-ray dense bundle: concave mirror (rays inside the sphere -> far + concave wall) focus at s=1.002 vs bundle s=1.003 vs analytic f=R/2=1.000; convex cap both diverge (focus at s~0). + 20x fewer rays for the same answer. (2) CAUSTIC: intensity ~ 1/pencil-area rises 1.4x -> 5.6x -> 90x -> 16000x + approaching the focus, symmetric around it. (3) GLOSSY/SOFT: one lobe sigma folds geometric spread (+) roughness (+) + light-angular-size in quadrature (0.020 -> 0.054 with roughness -> 0.062 with both) -- one Gaussian for the whole + secondary bundle. (4) DISPERSION: refracting the pencil at per-wavelength IOR fans red vs blue by 0.375 deg in crown + glass -- the chromatic split IS the frame diverging by colour. + +* KEY BUG FOUND (kept as a lesson). Reflection D2 = D - 2(D.N)N is INVARIANT to the sign of N, so a "concave flag" that + merely flips the normal does NOTHING -- whether a pencil focuses or spreads is decided by WHERE it hits (the outer + cap is convex/diverging like a mirror ball's fisheye; the inner far wall is concave/focusing), i.e. by geometry, not + a normal sign. The demo focuses by placing the ray origin INSIDE the sphere. + +* KEPT NEGATIVES (loud). (a) The caustic is a SINGULARITY: at the focus the pencil area -> 0 so geometric intensity -> + infinity; real caustics are finite (wave optics / finite aperture) -- the first-order model cannot bound the peak. + (b) This is FIRST-ORDER (linear) transport: exact for a thin pencil; it correctly EXHIBITS real spherical aberration + when traced against true normals (marginal rays don't all focus at the paraxial point -- a feature, validated by the + frame agreeing with the dense bundle which has the same aberration), but a fat pencil's higher-order spread is not + captured. (c) The glossy lobe is a Gaussian stand-in, not the true microfacet BRDF; good for the lobe WIDTH, not its + exact shape. + +CROSS-ARC: this is the VSA "we're in superposition, we already know the state, we just read the local neighbourhood +to augment it" on the optics side -- the frame is a cheap local probe whose transformed spread reconstructs an +analytic Gaussian instead of brute-force sampling. Complements the screen-space coherent-reflection module (that +reuses finished neighbours; this predicts the lobe a single ray stands for). + +## Glossy reflection in the shader + the reusable N-D pattern (deterministic-known -> sparse -> interpolate) (+6 tests, 2099->2105) + +Two things from Moose: (a) wire the ray-differential FRAME into the real shader; (b) generalise the recurring pattern +-- deterministic system, all information known, sparse probe + interpolate, dimension-agnostic -- into a reusable +primitive, with the 3D slime-mould maze as the motivating case ("3 dimensions is trivial when we have thousands"). + +### (a) Glossy reflection via the 5-ray frame (holographic_semantic) +NEW materials brushed/satin/glossy (reflective + ROUGH). `_roughness(mat_name)` gives a lobe half-angle; ctx carries +`rough`. In `_shade_rays`, a reflective pixel with roughness>0 no longer traces one sharp mirror ray -- it traces the +FRAME (centre + 4 marginal rays tilted by the roughness angle) and averages: a 5-tap reconstruction of the glossy lobe +(the pencil, wired in). Factored `_shade_reflection_rays` so the sharp and glossy paths shade identically. +* MEASURED: a brushed ball renders with a visibly BLURRED reflection (vs a sharp mirror ball). The 5-ray frame vs a + 64-ray Monte-Carlo glossy reference: PSNR 24.5 dB at 12.8x fewer rays, in a real 200^2 render. KEPT NEGATIVE: 5 taps + capture the lobe WIDTH (the blur) but not its full smoothness -- 24.5 dB, not 40; more taps close the gap. Caustics + and dispersion in the full SDF path are a heavier forward-transport job, deliberately not attempted here (the frame + module measures them analytically; only glossy is wired into the surface shader). + +### (b) The reusable N-D pattern (holographic_ndfield) +The recognition that several shipped things are ONE pattern -- a known deterministic field, probed sparsely, +interpolated, refined -- and that dimension is irrelevant once the operation is abstracted from coordinates. +* SEARCH: `grid_graph(shape, blocked)` builds an N-D grid adjacency dict; `solve_grid_maze` feeds it to the Tero flow + solver UNCHANGED (it only ever saw the graph). MEASURED: the SAME solver solves 2D (10x10), 3D (6^3), and 4D (4^4) + mazes with no new code; a 3D maze with an interior wall is solved corner-to-corner, unit steps, never through a wall. + The 2D maze the ant/flow shipped on is just D=2. +* RECONSTRUCT: `sparse_reconstruct(oracle, lo, hi)` samples a known field, reconstructs by Nadaraya-Watson (the + radiance field's own kernel), and REFINES where the reconstruction disagrees with the oracle (we can check -- the + field is known). MEASURED (3D field): adaptive sampling beats uniform at equal budget by 33% (120 samples) and 38% + (240 samples). This is the pattern under coherent reflection, ray differentials, radiance, and culling -- named once. + +CROSS-ARC: this is the thesis stated as a reusable tool -- "deterministic patterns within a system where we know all +information instantly give results, and we can interpolate them." The maze and the field reconstruction are the SEARCH +and INTERPOLATE faces of it; both are dimension-agnostic because the operation abstracts away the coordinates. 3D is +trivial with thousands of dimensions in hand. + +## Field-weighted navigation spread across domains + region-driven multi-material objects (+5 tests, 2105->2110) + +Moose: integrate the pathfinding/N-D pattern everywhere (physics, volumetrics, particles, navigating fields) and use a +COMPLEX multi-material test object to exercise several things at once. + +### Field-weighted navigation (holographic_ndfield) -- one primitive, many domains +The maze generalised: `field_weighted_graph(shape, cost)` builds an N-D grid whose EDGE COSTS come from a sampled +scalar field (base distance + the field crossed), and `least_cost_path` (deterministic Dijkstra) / `navigate_field` +find the least-cost route. The uniform maze is the constant-cost special case. `straight_line_cells` gives a +tie-break-independent baseline (a naive straight shot) to compare against. +* VOLUMETRICS: navigate a 3D smoke DENSITY blob -- straight-line crosses 2.75 density, navigated 0.00 (routes AROUND + the smoke). A dense wall with a gap: 5.50 -> 0.00 (finds the gap the naive path plows through). +* PHYSICS: navigate a POTENTIAL hill -- straight climbs 20.26, navigated 0.04 (routes around the peak). +* PARTICLES: the navigated route is world-space waypoints a particle follows (smooth, max step bounded). +* KEPT NEGATIVE: the route is GRID-CONSTRAINED (L1/Manhattan), so its raw cell count is larger than a Euclidean + straight line (40 vs 14 corner-to-corner) -- the honest quantity is the FIELD cost crossed (near-zero), not the cell + count; the diagonal-vs-Manhattan length gap is a discretisation artefact, not a routing loss. Also: the comparison is + vs a straight geometric shot, not vs the zeros-Dijkstra (which shares the router's tie-break and dodges obstacles for + free -- a subtlety that first hid the win). + +### Region field drives MATERIAL TYPE, not just colour (holographic_regionfield + shader) +`Region` now carries optional `reflect` and `roughness`; `RegionField.reflect_at` / `roughness_at` read them per point; +`_shade_rays` uses them so ONE body can be mirror in one region, brushed in another, matte elsewhere. MEASURED: a +single sphere renders as a genuine multi-material SHOWCASE -- matte body + a mirror cap (reflect 0.85) + a brushed patch +(reflect 0.55, roughness 0.16) + an ember + a glossy ice cap (5 reflectivity levels, 3 roughness levels on one surface, +_showcase.png). This is the complex test object Moose asked for: it exercises the region field, per-region materials, +the glossy frame, and reflection all in one render. HONEST SCOPE: regions drive reflect/roughness/albedo; making a +region GLASS (see-through refraction) would need the see-through path to be region-aware too -- deferred. + +CROSS-ARC: the navigation is the SEARCH face of the reusable N-D pattern applied to real fields (density, potential), +and the multi-material object is the region-field substrate carrying more than colour. Together they show the pattern +threading through volumetrics, physics, particles, and materials -- deterministic known field, probed, weighted, +routed; boundary says how to regard what's inside. Same primitives, many places. + +## Navigate a live scene, navigate raw market data, compose the route as a hypervector; game-engine panel consult (+4 tests, 2110->2114) + +Moose: do the two flagged follow-ons; ask the panel what game-engine lessons apply unconventionally; make navigation +work on RAW DATA (market) and be COMPOSABLE in VSA programs. + +### navigate_scene -- the SDF the renderer traces IS the cost field (holographic_ndfield) +`navigate_scene(sdf_eval, lo, hi, shape, start_world, goal_world, clearance)`: cells inside geometry (sdf<0) are +impassable; within `clearance` of a surface is costly; so the agent threads free space around objects. MEASURED: an +agent routed between two boxes takes 43 waypoints, min signed distance 1.93 (never inside geometry), and _scene_nav.png +shows the amber trail curving around the boxes with correct depth occlusion (projected waypoints depth-tested against +the union). This is the "one structure for drawing AND moving" lesson realized -- the nav cost field is the shader's own +SDF, not a separate representation. + +### Navigate RAW MARKET DATA -- same primitive, a data manifold instead of a scene +Real SOL 5-min prices -> a 2D (log-return, rolling-vol) state space -> an occupancy grid -> cost = -log(density), so +COMMON states are cheap and RARE states costly. navigate_field then finds the most-probable transition path between two +regimes. MEASURED: calm->stressed routes ~6-8% more probably than a straight shot through the state space. KEPT +NEGATIVE (loud): SOL's return/vol occupancy is essentially a vertical BAND (returns cluster near zero at every vol +level), so there is little manifold structure to exploit and the routing gain is modest -- on a curved/multi-modal +occupancy the win would be larger. The deliverable is the CAPABILITY: the identical navigator runs on raw data. + +### Composable in VSA programs -- a route is a hypervector (holographic_ndfield) +`encode_path(path)` binds each waypoint to its step index and bundles them (the engine's sequence encoding) -> ONE +hypervector; `decode_path_step` reads any waypoint back. MEASURED: a 35-waypoint scene path and a 28-state market path +each decode 100% of their waypoints back from the single vector. So a navigated route is composable VSA data -- bind it +to a label, bundle several routes, query order -- not just a Python list. This is the ECS->VSA-program idea: the route +is a role-filler structure over the shared field. + +### Panel consult (holostuff_game_engine_lessons.md) -- real published methods only +Verdict, highest impact first: (1) DATA-ORIENTED/ECS COMPOSABILITY -- treat scene, simulation, and dataset as ONE VSA +program over a shared field (Plate's HRR is literally this; grounds why navigate_scene + market-nav + encode_path are +the same move); (2) ONE shared spatial/graph structure for cull AND nav (Pharr's BVH-reuse) -- partly realized, the +field-weighted grid is the culling grid; (3) a UNIFIED constraint-projection solver (Macklin's XPBD/FleX) folding nav + +collision + physics into one iterate-to-feasible loop -- the biggest future build; (4) dirty-flag physics/nav deltas +(Milanfar/temporal coherence) extending the render delta discipline. Through-line: one representation, many consumers, +recompute only the delta -- holostuff's version is a composable VSA program over a shared field. + +### DEFERRED, honest: region-driven GLASS (see-through refraction) +The other flagged follow-on -- a region making a patch see-through (not just reflective) -- still needs the refraction +path to be region-aware, and it is the least aligned with this turn's raw-data/VSA/game-engine asks. Reflect/roughness +per region shipped last turn; per-region glass is deferred with this note kept loud. + +## Connectable parameters (a value can be a MAP/field, not just a number) + surface particle emitter (+8 tests, 2114->2122) + +Moose: (1) add a check that we can emit particles FROM a surface to drive particle systems; (2) parameters should take +MORE than a numerical input -- like every DCC app where a parameter can be a map / another node's output. + +### Connectable parameters -- the socket (holographic_param.py) +`Param(value=|field=|map=|source=)` is a parameter SOCKET, exactly the Blender/Houdini/Nuke affordance: type a number OR +plug in a texture MAP (ndarray sampled over a domain), a procedural FIELD (callable f(points)->values), or a wire to +another node's named OUTPUT (`source`, looked up in a ctx dict). `resolve_param(p, points, ctx)` is the ONE resolver: +scalar -> itself; callable -> evaluated; ndarray -> per-point values or a sampled map; Param -> dispatched on its live +channel (source wires are followed, dangling wires fall back to `default`). Backward-compatible: bare numbers still work +everywhere (a scalar resolves to itself), so existing call sites are untouched; a faculty opts in by calling +resolve_param. This is the panel's #1 (data-oriented composability) made concrete -- a parameter is just another edge in +the VSA program. +PROVEN in a real material path: `RegionField._scalar_at` now resolves reflect/roughness through the socket, so a +region's ROUGHNESS can be a field/map that varies across the surface (measured: roughness 0.05 low -> 0.275 high on one +region) while a constant param still returns a flat value -- "roughness = a texture", like any DCC material. + +### Emit particles from a surface (holographic_emitter.py) +Probe-first: there WAS a 2D `ParticleSystem` (force/advect) and a Poisson box sampler, but NO surface emission -- a +system had nowhere principled to spawn from. `emit_from_surface(sdf_eval, n, bounds, speed, weight, seed)` samples the +zero level-set of ANY callable SDF (project random candidates with a few Newton steps p -= sdf(p)*normal(p); +finite-difference normals; keep the converged ones), returns (positions, outward normals, velocities = normal*speed). +`advance(pos,vel,force,dt)` is the 3-D sibling of ParticleSystem.step. MEASURED (selftest + render): 240 particles land +ON a sphere (|sdf|<0.05), normals are radial, and BOTH emit params are sockets -- a WEIGHT map emits only from the top +hemisphere, a SPEED field makes crown particles faster; stepped under gravity they form a fountain (_emitter.png). This +is the "check we can emit from a surface to drive particle systems" Moose asked for, and it doubles as the emitter's +proof that parameters take maps/fields. + +### Order / panel note +Did these two explicit asks first (they were also the panel's #1 composability theme -- a parameter socket IS a +node-graph edge). Still outstanding from the panel's ranked list: the unified constraint-projection solver (Macklin's +XPBD/FleX, the biggest future build) and dirty-flag physics/nav deltas (temporal coherence) -- next in line. + +## Unified constraint solver was ALREADY shipped; the real gap was SDF/environment collision (+4 tests, 2122->2126) + +Panel next-item: the unified constraint-projection solver (Macklin XPBD/FleX + "everything is iterate-a-projection"). +PROBE-FIRST (the engine's own rule) found it ALREADY EXISTS and is already unified: `UnifiedMind.project_onto_constraints` +sweeps a list of projection callables (POCS/PBD), and it is explicitly the one engine under THREE faculties -- the SBC +resonator (alternating projection onto factor codebooks), `denoise(method='pnp')` (data-fidelity + manifold), and PBD +(the softbody delegates its `pbd` solve to it). Rebuilding it would have duplicated shipped work. So, honestly, the +panel's headline ask was done; the genuine gap the probe surfaced was narrower and real: + +### SDF / environment collision (holographic_collide.py) -- the missing constraint +The softbody resolved distance/bend/volume + node-node SELF-collision, but had NO collision with the ENVIRONMENT (an +arbitrary scene SDF) -- so cloth couldn't drape over a scene object and emitted particles couldn't pile on one. +`resolve_sdf_collision(X, sdf, radius)` pushes any point inside the collider (sdf=0; (b) a distance link + collision co-satisfy in one +project_onto_constraints sweep (60 sweeps, link residual <0.15, min sdf>=-0.02); (c) a corner-pinned cloth dropped on a +sphere DRAPES over the crown with min signed distance 0.02 (rests exactly at the collide_radius offset, NO penetration), +~14 nodes in contact, constraint residual 0.015 (_cloth_drape.png). This ties together the last three turns: the emitter +spawns particles from a surface, navigation reads the scene SDF as a cost field, and now collision keeps bodies OUTSIDE +that same SDF -- one geometry, three consumers, one projection engine. +KEPT NEGATIVE: the "outside a sphere" feasible set is NON-CONVEX, so POCS from a degenerate (near-coincident) start can +stall (two linked nodes settled 1.0 apart instead of 1.5); a non-degenerate start co-satisfies. Frictionless collision +also lets an unpinned cloth slide off a sphere (physically correct) -- the drape demo pins corners. + +### Panel status +Unified solver: shipped (confirmed by audit, not rebuilt). SDF collision: NEW, done. Still outstanding, next: dirty-flag +physics/nav deltas (temporal coherence -- recompute only the region of a field a moved object touched). + +## Dirty-flag physics/nav deltas + an above/below cross-pollination audit (+6 tests, 2126->2132) + +### Dirty-flag deltas (holographic_dirtyfield.py) -- the last panel item +The render discipline "recompute only what changed" (reprojection, the delta protocol) carried into the OTHER half of +a realtime engine: the navigation/physics COST FIELD. `DirtyField` holds an ADDITIVE field (base + sum of per-collider +penalties). When ONE collider moves, only the cells in its old ∪ new footprint change, so `move` subtracts the old +penalty array and re-evaluates the penalty ONLY in the new footprint -- O(footprint), and BIT-IDENTICAL to a full +rebuild. Additivity is what makes the delta exact (a min/union field's change isn't local -- documented in the module). +MEASURED: moving one collider re-evaluates ~208 cells regardless of grid size, while a full rebuild scales with area: +8.7x fewer evals at 30x30, 34.6x at 60x60, 138.5x at 120x120 -- the update cost is tied to the object's footprint, not +the grid. `cost_grid()` feeds navigate_field; re-routing on the updated field is correct. Faculty: `dirty_field`. +KEPT NEGATIVE: on an L1 grid a route can dodge a moved obstacle for free (edge-hugging), so "the path changed" is a +fragile assertion -- the test asserts the FIELD moved (old cell dropped, new cell rose) and the route stays valid. + +### Above/below audit -- applying the last 24h of changes elsewhere (holostuff_above_below_audit.md) +Probed each recent primitive (nav, navigate_scene, encode_path, Param sockets, emitter, SDF collision, dirty deltas) +for other application sites. Two genuine, high-value applications found and APPLIED: +* **Region ALBEDO through the parameter socket** -- last turn I gave reflect/roughness the socket but LEFT albedo a + bare constant (an inconsistency I introduced). `material_at` now resolves colour through `_resolve_color`, so a + region's albedo can be a field/texture (a colour gradient across the surface), consistent with reflect/roughness; + constant colours still work (backward compatible, shader unaffected -- 289 semantic tests green). +* **SDF collision in the 2D ParticleSystem** -- it could be force-driven and field-advected but couldn't avoid + obstacles. `ParticleSystem.step(collider=, collide_radius=)` now uses the same `resolve_sdf_collision` the softbody + uses; MEASURED: 2D particles raining onto a circular obstacle never penetrate it (worst penetration +0.05 over the + whole sim). The SDF-collision primitive now serves cloth (3D), the unified projection sweep, AND 2D particles. +Findings NOT applied (honest): fog_density as a Param (a fog map) -- plausible but the fog path is a separate render; +noted for later. DirtyField is itself the generalisation of the render deltas, so no other full-rebuild needed wrapping +(verify.py's Merkle rebuild is a different shape). The audit's lesson matched the engine's usual one: the highest-value +"new" work was making an existing primitive reach the places it already should have (albedo socket, particle collision). + +## Realtime rendering shortcuts: active-only ray marching (bit-exact 2.2x) + baked SDF grid (O(1) in complexity) (+5 tests, 2132->2137) + +Moose: chase realtime rendering speed -- shortcuts we aren't taking; cache/precompute; consider premade codebooks; make +changes with the LARGEST system-wide impact so cool tech doesn't get buried. PROFILED first (the engine's rule): a +200x200 trace did 3.84M SDF evals (96/pixel = the full step budget), each an O(n-primitives) union min, and the SAME +eval is paid again by shadows/AO/reflections/normals AND by navigation/collision/emission. The SDF eval is the shared +bottleneck. Two shortcuts found and applied, one bit-exact-and-universal, one complexity-scaling: + +### 1. Active-only ray marching (holographic_raymarch: sphere_trace + soft_shadow) -- the shortcut we weren't taking +The vectorised trace evaluated the SDF at EVERY ray every step, even rays that had already hit a surface or escaped past +max_dist. Now it evaluates ONLY the still-marching rays (drop a ray the step it converges/escapes). Since background +rays escape in a few steps and surface rays converge fast, the working set collapses quickly. MEASURED, BIT-IDENTICAL +(hit array identical, t max-diff 0.0): primary trace 5.7x faster; full render 2.1-2.3x faster (PSNR 99 at 200^2; 68 at +320^2 is adaptive-AA edge-selection float sensitivity, visually identical). This is a pure speed change with NO quality +tradeoff and it helps EVERY scene and EVERY traced pass. soft_shadow got the same treatment (bit-identical) but only +~1.02x -- shadow rays start from hit points and don't have the big escape-early population, so its active set barely +shrinks (kept anyway: correct, can only help). 31 render tests green; a regression guard test pins the bit-identity. + +### 2. Baked SDF grid (holographic_sdfbake.GridSDF) -- the realtime distance-field precompute +Bake the union onto a grid once, then TRILINEARLY sample it: a sample is O(1) regardless of #primitives (Unreal Global +Distance Fields / Redshift). `GridSDF` is a drop-in for the analytic union (.eval + .ids), so ONE bake speeds the +shader's trace/shadows/AO/reflections AND navigation/collision/emission. `render_scene(bake=res)` bakes (or reuses a +prebuilt GridSDF across frames). MEASURED: baked trace time is FLAT as primitives grow (analytic 0.16->5.9s from 1->64 +prims; baked ~0.9s throughout -> 6.4x at 64 prims), hit-agreement 0.998, depth-err 0.002. Amortised: a 24-object +8-frame orbit is 1.78x with one bake reused every frame. +KEPT NEGATIVES (loud): the bake is APPROXIMATE (surface detail below cell size blurs; PSNR 46/54/59 dB at 48/80/128^3) +and has an UP-FRONT cost, so for FEW-primitive single-frame renders it LOSES (5 balls, one frame: 0.3-0.7x -- slower). +It wins on COMPLEX scenes, MULTI-frame, or when one bake is shared across render+nav+collide. Navigation alone barely +benefits (1.0x) -- too few evals to amortise the trilinear overhead. Honest bottom line: pure NumPy/CPU won't hit true +realtime (that's the GPU/WebGPU muscle layer); active-only marching is the free, universal, bit-exact win that should be +on always, and the bake is the precompute for complex/animated scenes and the shared brain-layer distance field. + +### Faculties: `bake_sdf` (returns a GridSDF). render_scene gained `bake=`. + +## Over-relaxed (enhanced) sphere tracing -- opt-in, CONDITIONAL win with a kept negative (+1 test, 2137->2138) + +Continuing the realtime thrust: the textbook next shortcut after active-only marching is OVER-RELAXATION (Keinert et al. +2014, "Enhanced Sphere Tracing") -- step by relax*distance instead of distance, detect an overstep one step late (the +current sphere + the previous sphere no longer cover the step taken), back up to the safe edge and drop to normal steps. +Added as an OPT-IN `relax` param to `sphere_trace` (default 1.0 = the exact, bit-identical active-only path, untouched) +and threaded through `render_scene(relax=)` via ctx. + +MEASURED, honestly -- it is CONDITIONAL: +* OPEN scenes (few objects in space): NO win, sometimes slightly WORSE (relax 1.4 = 0.94x evals) -- active-only + compaction already captured the easy gain, and sphere tracing's natural large empty-space steps leave little for + over-relaxation while the overstep-backtracking adds evals. Hit-agreement 1.0 (safe), just no speedup. KEPT NEGATIVE. +* GRAZING scenes (a ground plane at a shallow angle -> rays skim the surface taking many tiny steps -- the technique's + designed-for case): 1.23-1.29x fewer trace evals, 1.36-1.41x faster full render. But hit-agreement drops to ~0.98 and + render PSNR is ~27-28 dB -- the classic over-relaxation artifact: a feature thinner than a grazing step gets skipped. + That quality cost is exactly why it is OPT-IN and default-off. +Compounds with the always-on active-only marching (2.2x), so a grazing scene reaches ~3x vs the original at a small, +opt-in quality cost. Correctly implemented (never misses catastrophically: the overstep test is provably safe, only the +surf_eps tolerance band is at risk on grazing hits). 300 render tests green (default path bit-exact). + +Honest bottom line: unlike active-only marching (free, universal, bit-exact, always on), over-relaxation is a +NARROW, opt-in speed/quality knob for grazing-heavy scenes -- kept because it's correct and measured, defaulted off +because it doesn't help most scenes and costs quality where it does. The measurement, not the intuition, decided the +default. + +## Precomputed Radiance Transfer -- "collapse, don't trace": relight via a dot product (+5 tests, 2138->2143) + +Moose: learn from quantum computing -- path tracing shouldn't be needed if we can "collapse wave functions"; search for +speedups / superpowers / capacity; more game-engine tricks; we aren't limited to 3-4 dimensions. Searched the web +(real methods, panel-attributed, honest hype filter -- see holostuff_quantum_gameengine_research.md). The concrete +answer to "collapse not trace" is PRECOMPUTED RADIANCE TRANSFER (Sloan/Kautz/Snyder 2002; Ramamoorthi 9-coeff +irradiance): the expensive part of GI is the per-point VISIBILITY INTEGRAL, which for a STATIC scene depends only on +geometry -- so precompute it once as a transfer vector in a spherical-harmonic basis, and runtime shading COLLAPSES to a +dot product of two ~9-element vectors. PROBE-FIRST first: holographic_harmonic is CIRCULAR (1D Fourier) harmonics for +VSA meaning, holographic_sphere is Riemannian geometry, holographic_radiance is a radiance FIELD -- true 3D-SH PRT was +absent. Built holographic_prt.py. + +WHAT IT IS: real spherical harmonics (sh_eval, bands 0-3, orthonormal to MC tolerance -- checked); project an +environment light onto SH (project_env_to_sh); precompute_transfer shoots hemisphere visibility rays per surface point +(spherical-Fibonacci low-discrepancy dirs) weighted by cosine, projected onto SH = the shadowed-diffuse transfer; +shade_prt(transfer, light_sh, albedo) = transfer @ light_sh (the collapse, no rays). VSA framing: the transfer vector is +a per-point codebook entry, relight is a projection/readout. HIGH-D angle: each point carries a 9/16/25-D TRANSPORT +vector -- more bands = more dimensions = more angular detail; the scene's light response doesn't live in 3D. + +MEASURED (self-shadowing cluster, 3914 surface points): precompute 3.82s ONCE; relight via PRT 0.0004s each (dot +product); relight via re-traced shadows 0.024s each -> 57x per relight (conservative -- PRT integrates the WHOLE +environment's visibility per relight vs one shadow direction). Rendered two lightings from ONE transfer +(_prt_lightA/B.png): warm-from-right vs cool-from-left, soft self-shadowing in the crevices, the second lighting a pure +dot product. KEPT NEGATIVES (loud): break-even ~160 relights, so PRT is for INTERACTIVE relighting / moving-light +animation over fixed geometry, NOT a single still frame (direct shading is cheaper there); LOW-FREQUENCY (SH truncation +-> soft ambient shadows, not crisp contact shadows); STATIC geometry (transfer tied to fixed points); DIFFUSE only +(glossy = per-point matrix + higher order, noted not built). Faculty: `radiance_transfer`. + +GAME-ENGINE tricks from the search: Lumen's software ray tracing merges meshes into a GLOBAL DISTANCE FIELD -- which +VALIDATES last session's SDF bake (a shipping AAA engine made the same choice). Next adjacent idea named: Lumen's +SURFACE CACHE (cache irradiance per surface patch, reuse across rays/frames -- PRT transfer vectors are a natural home). +Nanite = visibility-first deferred shading (the engine already shades hit points only) + cluster LOD (mesh-fork feature). +CAPACITY direction (documented, not built): tensor networks / MPS (Stoudenmire seat) -- quantum-inspired classical, and +non-unitary factorizations are strictly MORE expressive than unitary (Glasser 2019); a candidate for the next capacity- +cliff experiment via numpy.linalg SVD contraction, measured against chunk-and-re-anchor. HYPE FILTER: quantum HARDWARE +gives no speedup here; the value is entirely the quantum-INSPIRED classical structure (transport as a precomputed +operator; capacity as a tensor factorization). One measured build at a time -- PRT was the higher-certainty win. + +## Cost-to-go value field: "solve once in one place, read out anywhere" -- 3D nav = substrate value function (+6 tests, 2143->2149) + +Moose: build the highest-impact next thing, and remember that a 3D optimization applies to the STRUCTURE of holostuff in +general -- more accurate / faster / better bidirectional-context in ONE place should apply to EVERYTHING. PROBE-FIRST: +the tensor-network/MPS capacity direction is ALREADY built (holographic_tensor: tensor-product bind + tensor-train +truncation, Stoudenmire's comparison, with the honest negative that HRR gives up nothing per stored number) -- so that's +explored. The real through-line of the recent 3D wins (SDF bake, PRT) is "precompute/solve ONCE in one place, then every +query is a cheap read-out." The piece that generalizes it BEYOND 3D is a precomputed COST-TO-GO (value) field. + +BUILT in holographic_ndfield: `cost_to_go(nbr, edge_cost, goal)` runs ONE Dijkstra sweep FROM the goal over the whole +graph -> V (cost-to-go at every cell) + nxt (next step toward goal). `route_from(nxt, start, goal)` then routes from ANY +start by descent -- O(path), no re-search. `value_grid(V, shape)` materializes the potential. Undirected edges make +cost-to-go-from-y == cost-from-y-to-goal, which is what lets one goal-rooted solve serve every start. Faculty: +`cost_to_go_field` (returns V, nxt, and a route(start) closure). + +MEASURED (28x28x20 = 15680-cell field with an expensive ridge): ONE solve 0.107s + 8 descents 0.0002s = 0.107s vs 8 +per-start Dijkstra 0.541s -> 5.07x, and routes are PROVABLY OPTIMAL (identical field-cost to per-start Dijkstra). Each +descent is 3553x cheaper than a fresh Dijkstra search, so the win GROWS without bound as more agents route to the same +goal (break-even ~2 queries). Rendered _costtogo_field.png: the value field radiating from the goal, an L-barrier +avoided, 6 optimal descent routes from 6 starts -- one solve, six routes. + +THE GENERALIZATION (the point Moose asked for): the value field V is not a nav-only object. It IS a distance field (the +SDF is cost-to-go with unit cost + obstacles), a physics POTENTIAL (its negative gradient is a force), and an RL VALUE +FUNCTION (descent on it is the optimal policy). One precomputed field, many consumers -- the same "as above, so below" +shape as the SDF bake (one field: render+nav+collide+emit) and PRT (one transfer: every relight). DEMONSTRATED the +identical solver on a NON-3D 2D VSA cost manifold (a market occupancy grid): one solve routes every query state. So the +3D optimization is literally a substrate optimization. + +KEPT NEGATIVES (loud): the field is PER-GOAL (goal changes -> re-solve, exactly as PRT is per-geometry and the SDF bake +per-scene); for a SINGLE route to a unique goal, plain per-query Dijkstra is fine (break-even ~2 queries); grid/L1 +discretization inherits the earlier nav negative (the honest metric is field-cost-crossed, not cell count). + +## Composability of CALCULATION METHODS: dispatch the solver per-element, switch on the fly (+6 tests, 2149->2155) + +Moose: the engine already treats DATA as one substrate projected to whatever view is needed (a field is mesh OR fluid OR +point-cloud OR static collider, part-and-part by a map). Apply that SAME composability to WHICH METHOD computes a value: +trace to first hit (best for finding a surface), then at the bounce dispatch to whatever is best THERE -- collapse (PRT +dot product) on diffuse, trace on a mirror, glossy bundle on rough -- and SWITCH on the fly (a traced reflection landing +on diffuse collapses for the rest of the trip). PROBE-FIRST: the shader ALREADY dispatches by material (matte skips +reflection, mirror traces sharp, glossy traces a bundle -- via the `refl`/`rough` arrays), and the SUBSTRATE already +dispatches method by structure (`denoise(method='auto')` picks codebook/manifold/NLM; `decompose_signal` picks a basis +by topology). GENUINELY NEW: (1) collapse-vs-trace as dispatchable methods, (2) PER-ELEMENT selection by a field with +(3) on-the-fly switching mid-computation. + +BUILT holographic_dispatch.py: `dispatch_field(x, tags, ops)` -- the general primitive: gather each method's elements, +apply its op to the group (vectorised), scatter back; deterministic label order. `resolve_methods(ids, table, +region_field)` -- per-hit method tags from an object table with optional per-region override (part of ONE surface can be +mirror and part diffuse, by a map -- method composability at sub-object resolution). This is the per-ELEMENT +generalization of the whole-signal method='auto' the engine already had. Faculty: `dispatch_methods`. + +MEASURED (mirror sphere among diffuse spheres, 170x170): 4496 primary diffuse hits COLLAPSE (PRT), 1944 mirror hits +TRACE a reflection; of those, 276 reflection rays landed on a diffuse surface and SWITCHED to collapse for the rest (the +on-the-fly switch, counted). Precompute transfer at all collapse points once (4.32s); then per-relight DISPATCH 0.0036s +(all dot products) vs ALL-TRACE 0.0545s (re-shadow every frame) = 15.2x. CORRECTNESS gap the dispatch closes: pure- +collapse (PRT everywhere) CANNOT produce the mirror reflection; all-trace can but re-shadows every relight. Dispatch is +BOTH correct (mirror traced once) AND cheap to relight (diffuse + reflection-shading collapse) -- each method used where +it is best. Rendered _dispatch_lightA/B.png: a mirror reflecting the PRT-shaded diffuse spheres, two lights, the second a +near-free relight. + +KEPT NEGATIVES (loud): the collapse win is a RELIGHTING win (precompute transfer once ~4.3s; break-even is many relights +-- for a single still frame, direct shading is cheaper, same as PRT's own negative); the method field must be authored +or derived; bounded to one reflection bounce here; PRT's low-frequency limit still applies to the collapsed parts. THE +GENERALIZATION (Moose's mandate): `dispatch_field` is substrate-level -- the same primitive that dispatches shading +methods per hit can dispatch denoise/decompose/factor methods per element of any structure, which is what the engine's +whole-signal method='auto' already gestures at; this makes it per-element and composable, "part fluid, part static" for +COMPUTATION. + +## Wiring pass: recent work reachable through the mind AND the real pipeline, not hiding in tests/benchmarks (+1 test, 2155->2156) + +Moose: make sure everything is actually WIRED and usable to build ON TOP of holostuff -- not siloed in tests/benchmarks. +AUDITED the live code (grounded, not memory). Finding: the FACULTIES existed, but two things were siloed -- +`render_scene` (the shading pipeline) used NEITHER PRT nor dispatch (grep empty), and the hybrid collapse/trace renderer +lived ONLY in the measurement script (no reusable function). So PRT and method-dispatch were callable primitives with no +real render path, and `dispatch_field` was used only in tests. + +CLOSED THE GAPS: +- `holographic_dispatch.render_dispatch(sdf, camera, w, h, methods, colors, light)` -- the pipeline form of "collapse on + diffuse, trace on a mirror, switch on the fly": traces primary hits, dispatches per-hit via `dispatch_field` (so the + primitive is USED in the pipeline, not just tested), precomputes PRT transfer once for the collapse hits, and returns + (frame, relight, info) where relight(new_light) re-shades the collapsed parts for free. Wired as faculty + `mind.render_dispatch`. This is how PRT AND dispatch are now used in a real render. +- `render_scene_description` (the mind's text->render faculty) now passes `bake=` and `relax=` through to the module + render_scene, so the SDF-bake O(1) sampling and the opt-in over-relaxed marcher are reachable via the mind's render + path, not only the raw function. + +WIRING AUDIT (all YES now): active-only marching (in sphere_trace, every trace), SDF bake (bake_sdf + render_scene(bake=) ++ render_scene_description(bake=)), over-relaxation (render_scene(relax=) + render_scene_description(relax=)), PRT +(radiance_transfer + render_dispatch), cost-to-go field (cost_to_go_field returns V,nxt,route), method dispatch +(dispatch_methods + USED inside render_dispatch), hybrid renderer (render_dispatch faculty + module function). +`dispatch_field` and `render_dispatch` both now appear in holographic_dispatch.py + holographic_unified.py (not just +test_*). A single end-to-end integration test (`test_recent_faculties_are_wired_end_to_end`) exercises every one through +`UnifiedMind` with real calls and real outputs (a rendered frame, a route, a relight) -- the "build on top of holostuff" +contract, proven, not asserted. 282 touched-module tests green. + +## Adaptive render pipeline: ONE call that auto-selects methods, grounded in measured break-evens (+7 tests, 2156->2163) + +Moose: separate options are useful, but I want everything integrated into ONE pipeline that automatically adapts. Built +the top of the composability stack: holographic_adaptive.py DERIVES the dispatch decisions from the scene + workload +instead of the caller choosing bake/relax/collapse/trace by hand. Grounded in the break-evens already MEASURED, not +guessed, and returns a PLAN with a reason for each choice so the automation stays legible. + +`plan_render(objects, frames, relight)` -- the pure, testable decision layer: +- BAKE the SDF (O(1) sampling) iff primitives >= 16 OR frames >= 4 (measured: bake loses <~16 prims single-frame, wins + 6.4x at 64 prims and 1.78x at 24 prims x 8 frames). Else analytic SDF (cheaper). +- RELAX stays 1.0 (exact active-only marcher) -- over-relaxation is a grazing-only, quality-costing manual opt-in; the + measurement set its default off, so adaptive never turns it on by itself. +- COLLAPSE vs TRACE per surface AND workload: single frame -> 'trace' path = render_scene, which already dispatches + matte/mirror/glossy per hit (direct shading is cheaper than a PRT precompute for one frame); relighting -> 'dispatch' + path where each surface's method is DERIVED from its material reflectivity (matte 0.0 / plastic 0.12 / ceramic 0.18 / + default 0.2 -> COLLAPSE as diffuse, free relight; glossy 0.35 / metal 0.45 / mirror 0.85 -> TRACE). Threshold 0.3 + (set after finding MATERIAL_RENDER[None] carries a 0.2 default sheen -- probe-first caught it). + +`render_adaptive(objects, camera, ..., frames, relight, light)` EXECUTES the plan: single frame -> render_scene with the +auto-bake/relax; relight -> builds the object-only union + colors, derives methods, calls render_dispatch, returns a +relight handle. Returns (frame, relight_or_None, plan). Faculties: `render_adaptive`, `plan_render`. Verified end to end: +a mirror+diffuse scene auto-plans {mirror:trace, diffuse:collapse}, renders both, relights free (_adaptive_A/B.png). + +KEPT NEGATIVES (loud): thresholds are heuristics at the measured break-evens (named constants, easy to audit/tune, not +learned); collapse treats a slightly-reflective surface (e.g. the 0.2 default) as pure diffuse, losing its faint sheen +when relighting (the diffuse approximation, PRT's own low-freq limit); the relight-vs-single decision is the caller's +`relight` flag (the pipeline can't see the future -- if you'll relight many times, say so and diffuse precomputes once). +The SEPARATE options remain for manual control; this is the automatic default on top of them. Also a pre-existing +parse_description quirk surfaced (chained "A beside B beside C" can overlap two objects) -- noted, not fixed here. + +## Distributed computation: reassembly IS the computation's commutative monoid (+7 tests, 2163->2170) + +Moose: learn from distributed computation (SETI@home, Folding@home, distributed rendering). His rendering experience: +break a job into BUCKETS, precompute caches (GI/irradiance) on the MAIN machine, ship to nodes to crunch rays, +REASSEMBLE. holostuff can make many (even nested) HoloMachine VMs -- so distribute to VMs like DR, EXCEPT buckets could +SHARE overlapping results and we can SHORTCUT the reassembly. Extrapolate to the whole architecture; apply to +particle/fluid/fire/soft+rigid/attractors/fields/everything, adaptively. + +Web-searched the DR literature: V-Ray precomputes Light Cache/Irradiance Map on ONE machine -> shared storage -> DR +final pass (avoids re-solving GI per node); bucket SEAMS come from mismatched colour management between nodes; the +irradiance cache is a SHARED MUTABLE structure "notoriously difficult to parallelise" (Warwick, EGPGV 2006) because +nodes must exchange samples. + +THE VSA-NATIVE INSIGHT (what makes holostuff sidestep both the seam problem and the shared-cache-communication problem): +most holostuff computations are COMMUTATIVE MONOIDS, so the "shortcut reassembly" Moose intuited is literally the +monoid's operator -- no separate stitch pass to get wrong. + * force / attractor / potential / radiance / density fields ADD -> reassemble by SUM (linear superposition) + * an SDF union is a MIN over primitives -> reassemble by MIN + * occupancy / coverage is a MAX -> reassemble by MAX + * a VSA scene / memory is a BUNDLE of its parts -> reassemble by BUNDLE +Every reduce is associative + commutative => the result is INDEPENDENT OF BUCKET ORDER, which is exactly the property +that lets buckets run on separate machines/VMs and combine with no stitching, no seams. The shared cache here is +READ-ONLY (an SDF bake, a cost-to-go field, a codebook, a PRT transfer): compute once on the main node, hand the same +immutable object to every bucket -- so none of the irradiance-cache inter-node sample-exchange overhead. + +BUILT holographic_distribute.py: partition(n,k), adaptive_partition(costs,k) (LPT load balance -- isolate heavy items so +the slowest bucket, which bounds farm wall-time, is minimised), reducers reduce_sum/min/max/bundle, distribute(buckets, +worker,reduce,cache) (commutative-monoid fold), distribute_scatter(...) (disjoint tiles/subsets). Faculties +distribute_compute, partition_domain. + +MEASURED (correctness against the monolithic result, kept loud): + * FORCE FIELD (sum): partitioning the SOURCES and summing == monolithic, exact to 1e-12; shuffle bucket order -> + identical to 1e-12 => distributable. + * SDF UNION (min): partitioning primitives and taking min == monolithic, BIT-EXACT; shuffle -> bit-identical. + * SHARED-CACHE TILED RENDER (scatter): bake the SDF ONCE (the main-node GI cache), tile the frame, every tile reads + the SAME immutable GridSDF, scatter-reassemble -> BIT-EXACT to the monolithic render, shuffle tiles -> identical, NO + SEAMS (the shared deterministic cache is why borders agree -- the DR seam failure mode can't occur). _distribute_tiled.png. + * DISTRIBUTED VSA MEMORY (bundle): build a key->value memory in buckets, reassemble by bundle == monolithic memory + direction (cos > 0.99), unbind still recovers every value. + * adaptive_partition isolates the heavy item; never worse than an even split. + +KEPT NEGATIVES (loud): (1) in-process SEQUENTIAL -- the sandbox has no cluster, so NO multi-node speedup is claimed or +measured (the tiled render is actually slower in-process from Python per-bucket overhead); what is built and proven is +the ARCHITECTURE that makes distribution correct -- bucket independence (order-invariance), read-only shared cache +reused once, reassembly = the monoid op. (2) SUM reassembly is order-independent only to FLOAT ROUNDING (~1e-12), not +bit-exact -- float addition isn't associative, which is why render farms pin accumulation order for bit-identical +frames; MIN/MAX/scatter ARE bit-exact order-independent. (3) exact superposition holds only for the LINEAR/monoid part +of a computation -- a nonlinear step with feedback (an implicit solve, a collision resolve) does NOT superpose and must +be distributed differently (disjoint scatter, or a shared-cache pass then a local solve). The generalisation to +particles/fluid/fields is precisely: distribute the LINEAR field/force accumulation by superposition (exact), keep the +nonlinear integrate/solve local per bucket over a shared field. + +## Bit-exact reassembly via more accumulator bits + explicit bake_scene (first render = relight) (+2 tests, 2170->2172) + +Moose pushed on two things. (1) The bit-exactness negative can be REMOVED by carrying the accumulated value in more +dimensions/bits ("additional dimensions that carry accumulative values"). Tested honestly and CONFIRMED: float sum +reorders at ~1e-13 (not associative), but quantizing each contribution to fixed-point at a data-independent scale (peak +is order-independent) and summing in INT64 -- where addition is exact + commutative -- is BIT-EXACT regardless of bucket +order, agreeing with the float sum to ~1e-9. Built `reduce_sum_exact(parts, bits=40)` in holographic_distribute and +wired `reduce='exact'` into distribute_compute. TRADE (kept honest): a uniform quantization set by `bits` (finer with +more bits, bounded by int64 range, guarded against overflow) and a bounded dynamic range; use it when bit-identical +frames across nodes matter, plain reduce_sum when ~1e-12 is fine. This is the engine's usual move -- spend extra +dimensions/bits to buy an exact property -- applied to accumulation, so Moose's claim holds. (NOTE: this fixes the +PRECISION/reproducibility limit; it does NOT make a NONLINEAR operator linear -- collisions/implicit solves still don't +superpose, that's maths not rounding. The holographic handling there is distribute the linear field by superposition, +reconcile the nonlinear part locally / by denoise-and-auto-select over a shared field.) + +(2) A BAKE step callable BEFORE the first render so a builder's first frame is a RELIGHT, not a cold calculation. +Factored render_dispatch's precompute into an explicit two-step API: `bake_scene(sdf,camera,w,h,methods,colors)` -> +`BakedScene` (traces primary visibility ONCE, dispatches per hit, precomputes PRT transfer for every diffuse hit and the +diffuse surfaces behind each mirror bounce), and `render_baked(scene, light)` (pure dot-product relight, no tracing). +render_dispatch is now a thin wrapper = bake_scene + render_baked (behavior byte-identical, all prior tests green). +Faculties `bake_scene`, `render_baked`. A builder calls bake_scene at scene-load; every frame after -- including the +first -- is the cheap relight. Verified: explicit bake+render_baked == render_dispatch's first frame; relighting a second +light changes only shading. This is "collapse not trace" taken to its conclusion: bake once, all renders are relights. + +## Audit: dimension-bound limits + 2D tiles / 3D bricks (parallelism to 2D/3D) + the L-cache/RAM hierarchy (+4 tests, 2172->2176) + +Moose asked for an AUDIT: (a) which other limits fall to "just use more dimensions", (b) extend render-bucket parallelism +to 2D tiles and 3D bricks, (c) where more dimensions buys SPEED, (d) how our L1-L4/RAM cache analogues interact. + +AUDIT -- limits that ARE dimension-bound (more dimensions genuinely relaxes them): + * bit-exact reassembly -- DONE (reduce_sum_exact: more accumulator bits -> exact). The template for the rest. + * bundle/superposition capacity cliff -- HRR SNR ~ sqrt(D), so more D = more items before noise wins. + * resonator factorization ceiling -- more D or SBC blocks = more factors x alphabet before the combinatorial cliff. + * index/hash collisions (RouteIndex RAM-regime, HoloForest) -- more hash bits = fewer collisions = fewer O(n) fallback + scans -> also a SPEED win. + * int8 quantization geometry distortion -- more bits (the 'rd' code) = less distortion (same lesson as exact-sum). + * SDF-bake resolution / PRT SH order / splat count -- more grid cells / coefficients / splats = more accuracy. +AUDIT -- limits that are NOT dimension-bound (honest; more dimensions does not help or hurts): + * nonlinearity (collision resolve, implicit solve don't superpose) -- maths, not precision; handle locally per bucket. + * non-convex POCS stall from a degenerate start -- geometry/optimisation. + * the bake break-even / up-front cost -- more dimensions (finer bake) makes the up-front WORSE, not better. + * CPU/NumPy realtime ceiling -- hardware/language; more dimensions cost more compute. + * missing signal (propagator prediction near-tie on returns) -- no structure to exploit; dimensions can't invent it. +AUDIT -- more dimensions for SPEED (yes, in three shapes): (1) precompute more channels -> O(1) query (SDF bake, PRT, +cost-to-go -- the "bake" pattern); (2) more hash/index dimensions -> fewer collisions -> fewer fallback scans; (3) +capacity headroom (higher D -> higher SNR) -> fewer cleanup/resonator ITERATIONS to converge. + +BUILT -- render-bucket parallelism extended to 2D and 3D (holographic_distribute): + * partition_2d((H,W),tiles) -> disjoint (row,col) TILES (images/2D fields); partition_3d((X,Y,Z),bricks) -> disjoint + (x,y,z) BRICKS (SDF grids, volumes, fluid grids); distribute_bricks(out_shape,regions,worker,cache,skip) places each + region's result (disjoint => order-independent, seamless), with an optional skip() for EMPTY bricks. + * Faculties partition_grid (2D or 3D by shape), distribute_bricks. +MEASURED: 3D brick bake == monolithic dense bake BIT-EXACT; shuffle brick order -> identical (=> bricks distributable to +separate VMs). 2D tiles == monolithic. + * SPARSE-BRICK SKIP is the real 3D-brick speed win (beyond parallelism): a brick with no surface (|sdf(center)| > + brick half-diagonal) is dropped. LARGE volume + one small ball -> 89% of bricks skipped, surface cells identical, + 1.39x. KEPT NEGATIVE: at HIGH occupancy (3 balls near the centre of a modest volume) only ~16% skip and the + skip-test overhead cancels the saving (~1.0x) -- sparse-skip wins only when occupancy is LOW (the common + small-object-in-big-volume case), the same conditional-amortisation shape as the SDF bake and PRT. + +L-CACHE / RAM HIERARCHY (audit of what we have + how bricks tie in): the L1-L4/RAM names in the engine are an honest +ANALOGY (Python can't touch real CPU caches) -- holographic_anim/deltachain use L1/L2=hot-full, L3=delta, L4=recompute +for FRAME storage; holographic_tree has the RAM-regime router (floor-divide a coordinate -> its tile, O(1), no search); +meshbridge does cache-blocking to a ~6MB working budget. The tile/brick decomposition connects to all three: a brick IS +a cache-block (size it to the working budget so it streams), a brick's address is the RAM-regime floor-divide (O(1) +lookup, no search), and baked bricks can live in the tiered store (hot bricks resident L1/L2, cold as deltas L3 / +recompute L4). Not separately rebuilt this pass -- flagged as the honest next wiring (a baked-brick cache with tiered +eviction) rather than claimed as done. + +## Above/below audit + the material tie-together: pattern (Moose's module) + SurfaceMaterial/render_surface (+9 tests, 2176->2185) + +Moose called the above/below audit: silos in tests/demos/benchmarks, duplicates, and MISSING FIRST-CLASS OBJECTS that +tie functionality together. Grounded in greps of the live code + his two docs (the demo-gallery handoff CORE_NOTES and +the modeling-gaps doc). + +AUDIT FINDINGS (honest): +* The arc's earlier passes already killed the worst silos (render_dispatch was benchmark-only, dispatch_field + test-only -- both fixed previously). Test-file helper defs are scaffolding, not capabilities. One Camera, one + save_png (no duplicates). +* `noise_field` vs `pattern.value_noise` is NOT a duplicate: hypervector-native noise (field IS one vector, queried + through an encoder) vs a plain spatial callable for Param sockets -- same concept at two levels, as above so below. + Cross-linked in the noise docstring so no future session "deduplicates" them wrongly. +* THE REAL GAP (CORE_NOTES 2.1, "the biggest"): the render-channel material was a DICT (MATERIAL_RENDER) with no + object resolving channels per hit -- demo backends kept re-implementing exactly that. + +LANDED: +1. `holographic_pattern.py` -- Moose's module, VERBATIM, demo-gallery -> core. Named deterministic pattern FIELDS + (checker/stripes/gradient/dots/value-noise/fbm) as plain callables f(P)->[0,1]; integer-lattice hash (pure int64 + arithmetic, PYTHONHASHSEED-independent, seed-perturbed); PATTERNS registry + make_pattern (serialized specs) + + field_lerp (drive a channel lo..hi). Faculty `pattern_field`. +2. `holographic_surface.py` -- the first-class citizen: `SurfaceMaterial` with EVERY channel a Param socket (color, + roughness, reflect, emission, opacity; each a const / Param / callable field / map), `from_name` consuming the ONE + canonical MATERIAL_RENDER table (dedup: single source, no per-demo copies), `.resolve(points)` = THE per-hit + channel resolution demos hand-rolled, and `render_surface(sdf,camera,w,h,materials)` shading per hit: Lambert + + Blinn (roughness -> highlight width) + environment reflection by `reflect` + emission + opacity alpha-composited + over ONE continuation ray. The tie-together: pattern -> Param(field=) -> channel -> per-hit resolve -> pixel. + Faculties `surface_material` (name + overrides), `render_surface`. Verified visually: checker albedo wraps a + sphere as a true SOLID texture (no UV unwrap), fbm drives metal roughness, transparency composites + (_surface_material.png). +KEPT NEGATIVES (loud): render_surface reflections sample the ENVIRONMENT only (object-object mirrors live in +render_dispatch / render_scene -- use those for a mirror that shows the scene); opacity is ONE transparency layer +(stacked glass needs the full renderer); resolve_param flattens (M,3) colour fields to (3M,) -- _rgb detects by size +and reshapes (a param-API wart worth fixing at the source someday, noted not fixed). + +REMAINING THREADS from the two docs -> appended to WIRING_BACKLOG.md ranked by their own build order (progressive +path_trace, SDF->splat bridge, RenderSession, pick()+AOVs, TRS state + incremental rotate/scale, vectorised PBD (the +measured 83 ms bottleneck), SimulationSession, predicate RegionField, dual contouring, per-vertex normals). Recorded, +not silently dropped; each is a build, not a wire, and lands with its own close-out. + +## Above/below audit, part 2: the RenderSession keystone + progressive path_trace (+7 tests, 2185->2192) + +(Same audit request re-sent; probe-first confirmed NO rollback -- last session's pattern.py + SurfaceMaterial/ +render_surface were intact on disk and in the zip. So this pass ADVANCES the audit to the next tie-together instead of +redoing it.) + +AUDIT (CORE_NOTES section 3): the rendering threads were DISCONNECTED -- render_surface (fast preview, landed last +session), path_trace (photoreal final, but run-all-then-return, no progress), field_to_splats (browser proxy, but +wanted pre-sampled points not an SDF), and no object holding a scene so a demo hand-wired them and preview/final could +silently drift. The doc's ranked keystone -- a first-class RenderSession -- did not exist (probe: 0 matches). + +BUILT: +1. Progressive path_trace (the one real gap, prereq #1). Added backward-compatible `on_progress(running_image, done, + spp)` + `progress_every` to the EXISTING path_trace (no duplicate) -- default off is BYTE-IDENTICAL to before + (test pins it); on, it hands back the running mean every k samples (the refine stream). Final image unchanged. +2. `holographic_session.py` -- the keystone. `RenderSession(sdf, materials, camera)` owns ONE scene (SDF + a + SurfaceMaterial per object id + camera) and derives every output from it so they can't diverge: + * `.preview()` = render_surface (fast material preview) + * `.render_final(spp, on_progress=)` = path_trace with the progress hook, using the SAME SurfaceMaterials via a + material adapter (_pathtrace_material: color->albedo, reflect->metallic, roughness->roughness, + emission*color->emission, opacity<0.5->ior 1.5 = real refractive glass in the tracer) + * `.to_splats()` = sdf_surface_points + field_to_splats -> a coloured splat proxy for a browser billboard + shader (the "everything-moves preview" answer: surfaces ARE splats) + * `.edit_channel(id, channel, value)` / `.set_material` = a live edit that shows in BOTH preview and final + because they read the same materials (the modeling-loop live-material ask) + Also `sdf_surface_points(sdf, bounds, n)` -- the missing FRONT HALF of the SDF->splat bridge (random points + one + Newton step onto the zero level, kept where |sdf|metallic and opacity->glass mappings are pragmatic approximations (a reflective plastic +reads slightly metallic in the final); sdf_surface_points is rejection-sampled so very thin features may need a bigger +oversample. The progressive final is a VALID refine (same seed/spp -> same final as a one-shot path_trace, test-pinned). + +REMAINING from the docs (still in WIRING_BACKLOG.md, unchanged): pick()+id/depth AOVs, per-object TRS + incremental +rotate/scale, vectorised PBD (the measured 83ms bottleneck), SimulationSession, field-predicate RegionField, dual +contouring, per-vertex normals. The RenderSession is the render half of the "session" idea; SimulationSession is its +stepped-sim twin and the next natural build. + +## Physical-definitions fork integrated: matlib + quantities + definitions (+57 tests, 2192->2249) + +Moose brought a fork with a grammar and definitions of physical things. FIRST, an honest correction to my own record: +I initially reported the fork's modules as "missing from the zip" -- that was WRONG. My first extraction was partial +and I claimed absence without checking the archive listing; the zip in fact held 1375 files including all three new +modules. Re-extracted clean, verified against the archive. (Lesson pinned: verify against `unzip -l`, never infer +absence from a partial extract.) + +THE THREE NEW MODULES (dropped in verbatim; their 55 shipped tests pass unchanged in our tree): +* `holographic_matlib.py` -- a ~130-preset RENDER material library (glTF metallic-roughness): diffuse/metal/wood/ + stone/glass/gem/emissive/biome/layer/deposit/liquid/fabric/organic. `material(name)`->PBRMaterial, `by_class`, + `biome_at` (Whittaker elevation/temperature/moisture classifier), and `fractal_planet` (a fBm-displaced sphere + painted by biomes, wrapped around crust/mantle/core shells with ore-deposit pockets -- region composition all the + way down). +* `holographic_quantities.py` -- the dimensional GRAMMAR: `Quantity` = value+unit+uncertainty+source, multiply + composes dimensions (density*volume->mass), add requires matching dimensions (length+mass refused at the '+'), + conversion is one call, uncertainty propagates; recipes `bill_mass/bill_cost/bill_embodied_carbon`. Extensible via + `register_unit`. QUDT/UCUM/astropy boiled down to an exponent vector + SI multiplier, NumPy+stdlib only. +* `holographic_definitions.py` -- a DEFINITION LIBRARY: `MATERIALS` (real density/viscosity/Young's/refractive/sound- + speed/specific-heat per named material), physical relations (float/sink/suspend/flow), and `resolve_scenario(desc)` + -> a validated Scenario that grounds named things, checks the physics, and emits `build_spec()` (phenomenon + solver + family + per-body masses/volumes) for the shipped solvers. Plus `data/definitions/` (native material json + a + minerals sample + README) -- the extensible, people-can-add-more definition data. + +INTEGRATED INTO OUR SYSTEM (the "plug directly in" ask): +* RENDER: `SurfaceMaterial.from_matlib(name)` bridges a glTF PBRMaterial into our first-class render material + (base_color->color, alpha->opacity, metallic->reflect, roughness->roughness, emissive->emission), so ALL ~130 + physical presets + planet biome materials drive render_surface / path_trace / RenderSession. Faculties + `render_material`, `material_catalog`, `fractal_planet`. Verified visually: gold/ruby/marble path-traced through the + RenderSession (_matlib_final.png); a fractal-planet cross-section with interior shells + ore pockets + biome crust + (_planet_section.png). This is "renders data-driven with physically accurate things" and it also completes the + render arc -- the session is now fed by physical definitions, not hand-set colours. +* SIM: `physical_material(name)` (solver-ready property dict) and `resolve_scenario(desc)` (a physically-validated sim + spec: wood floats consistent=True; a steel ball "floating" is consistent=False with the density reason, and still + emits a buoyancy solver spec). "Simulations data-driven with physically accurate things." +* GRAMMAR: `quantity(value, unit)` and `estimate_bill(bill)` -- the house bill composes to the documented 56.7 t / + $14,430 / 17,276 kgCO2e, densities REUSED from the definition library (not duplicated), sample cost/carbon factors + flagged pending a real USGS/ICE ingest. + +KEPT NEGATIVES (from the fork, kept loud): matlib presets are hand-authored plausible PBR values, not measured spectra; +the planet is fBm relief, not plate-tectonics/climate sim; deposits are noise-threshold pockets; fBm queries are +per-point Python loops so cross-sections are coarse. quantities: currency has no FX; sample price/carbon are +placeholders. definitions: sources disagree in reality, so provenance/uncertainty fields exist but the offline tables +are single-source. The sandbox CANNOT reach the external science APIs (allowlist is github/pypi/npm) -- live ingest is +designed as an adapter interface, only the offline composition machinery is exercised (the constructed-vs-measured +boundary the project always keeps). + +STILL ON THE BACKLOG (unchanged): SimulationSession (now has physically-accurate params to consume via resolve_scenario +/ physical_material -- the natural next build), RenderSession.preview via IncrementalRenderer, vectorised PBD, pick()+ +AOVs, ingest adapters (network-gated). WIRING_BACKLOG.md updated. + +## Material structure primitives M1/M2/M3 (grain, cells, inclusions) -- the cheap, thermo-independent ones (+11 tests, 2249->2260) + +From the Material Structure & Process backlog. PROBE-FIRST split the seven items cleanly: M4-M7 (oxidization +process, phase change, material-specific fire, burn/decay) all chain onto a thermodynamics heat model (T1/T3/T4) +that is NOT built yet -- so they are NOT cheap now. M1-M3 are independent structure/appearance primitives that +"ship anytime": pure sockets f(points)->value that plug into SurfaceMaterial(color=Param(field=...)) and render +through render_surface / RenderSession. Knocked out all three. + +PROBE FINDING (why probe-first matters, again): M1 `holographic_grainmat.py` ALREADY EXISTED and passed its own +selftest -- but was SILOED (0 references in the mind, no test file). So M1 was a WIRING job, not a build. + +LANDED: +* M1 grain -- WIRED the existing grainmat: faculty `grain_material` (wood_albedo socket: concentric rings + fibre + streaks + fBm domain-warp knots, volumetric in object space so a cut board shows continuous rings), plus a test + file pinning the volumetric property (varying ALONG the axis barely changes the ring value; varying the radius + sweeps rings). `substrate_layers` (plywood/strata) exposed via the same module. +* M3 inclusions -- NEW `holographic_inclusions.py`: `with_inclusions(base, [(material, fraction, scale), ...])` -> + a socket that paints base except in noise-blob pockets (carbon in steel, veins in stone). The honest bit: + coverage is CALIBRATED -- the threshold is the (1-fraction) quantile of the noise over a fixed calibration + sample, so measured coverage hits the target (bar met: 0.1/0.25/0.4 within 0.05). Reuses the planet's ore-deposit + noise-threshold pattern, scoped to a material. Faculty `material_inclusions`. +* M2 cells -- NEW `holographic_cellular.py`: Worley/Voronoi `VoronoiCells` (nearest-seed id + F2-F1 edge metric), + `cell_albedo` (per-grain facet colour by deterministic id-hash, darkened to a crack colour on boundaries), + `crack_mask`, and `lattice(motif, period)` (Bravais-style object-space modulo packing, tiles BIT-EXACTLY). + Faculty `crystal_material` (returns (cells, socket)). Bar met: socket ids == brute-force nearest seed; crack + metric ~0 on a bisector; lattice motif(P)==motif(P+period). +All three verified rendering through the pipeline together (_structmat.png: wood grain | gold+quartz in slate | +cracked polycrystal). + +KEPT NEGATIVES (loud, from the backlog): these are APPEARANCE/structure models, not first-principles -- grain is +procedural noise (not xylem growth); Voronoi cells are a facet/crack LOOK (not atomic unit cells with real lattice +constants or diffraction); inclusions are spatial+statistical placement (not a metallurgical solidification model), +and the alloy fraction is a placement statistic, not a thermodynamic composition. All deterministic, NumPy+stdlib, +additive. + +NOT DONE (correctly deferred): M4-M7 need the thermodynamics heat model first (temperature/pressure source) -- +oxidization front (reaction-diffusion on HyperCA), phase change (latent heat), material-specific combustion +(autoignition/soot data + ignite->emitter coupling), object burn/decay (mass loss over time). Those are the +"process" layer; build the thermo backlog (T1/T3/T4) before them. Recorded in the backlog ordering. + +## Thermodynamics foundation T1/T3/T4 (gas state, blackbody, heat) -- the trigger layer the material processes need (+13 tests, 2260->2273) + +Moose greenlit building the thermo foundation FIRST, so the material-process layer (M4-M7) has a temperature/ +pressure source to trigger on. PROBE-FIRST: no thermo doc, no thermo modules existed (clean build); definitions +already carry `specific_heat` (T4 ready) but NOT `thermal_conductivity` (it lives in data/definitions/.../enrich.json, +which build_standard_library does NOT auto-load) nor gamma/molar_mass. Units K/Pa/J/W exist in the quantities +grammar; mol absent (avoided via the specific-gas-constant form). + +BUILT (three self-contained physics solvers, NumPy+stdlib, readable with the physics commented per Moose's pref): +* T3 `holographic_blackbody.py` -- temperature -> the colour it GLOWS, first-principles: Planck's law integrated + against ANALYTIC CIE colour-matching curves (Wyman 2013 multi-Gaussian fit, no data tables) -> XYZ -> sRGB. + Verified: 1000K ember red (1,0.19,0), 6500K daylight near-white, 12000K blue-white; Wien peak correct (Sun ~500nm, + ember in IR). Faculty `blackbody_color`. This is the ember/flame/glowing-char colour M6/M7 will paint by temperature + (_blackbody_strip.png). +* T4 `holographic_heat.py` -- Q = m c dT (specific heat REUSED from definitions), Fourier conduction + dT/dt=alpha*laplacian(T) (alpha=k/(rho c)) with AUTO-SUBSTEPPING so any dt stays stable, insulated boundaries + (total heat conserved), and Newton cooling (dT/dt=-(hA/mc)(T-ambient)). `material_thermal(name)` reuses + definitions density+specific_heat and reads thermal_conductivity from enrich.json (steel 50, water 0.6 -- NOT + restated). Faculties `diffuse_heat`, `heat_body`, `material_thermal`. This is the keystone: M5/M6/M7 all trigger on + temperature from here. +* T1 `holographic_gas.py` -- ideal gas law PV=m R_specific T (R_specific=R/M, avoids mol), adiabatic PV^gamma / + TV^(gamma-1) (compression heats), speed of sound a=sqrt(gamma R_specific T), and boiling point vs pressure + (Clausius-Clapeyron). CROSS-CHECK win: derived speed of sound in air = 343 m/s, which independently matches the + definitions' tabulated air sound_speed of 343 -- two roads, one number. Boiling point 100C@1atm, 90C@70kPa + (mountain), >100C in a pressure cooker -- the fact M5 consumes. gamma/molar_mass are the new gas data columns + (GAS_PROPERTIES table, real values). Faculties `ideal_gas`, `boiling_point`. + +Also found + noted (not yet wired): build_standard_library does NOT ingest enrich.json, so lib.get('steel').props +lacks thermal_conductivity even though the data file has it. The heat module reads enrich.json directly +(non-duplicating); wiring a general enrichment loader into the library is a small additive follow-up (the extensible +"drop a json, gain a column" path). + +KEPT NEGATIVES (loud): blackbody is an ideal emitter to sRGB (no spectral flame lines, emissivity 1); heat model has +constant properties, explicit-scheme (auto-substepped), insulated default boundaries, no radiation/convection here; +gas is IDEAL (no Van der Waals), constant gamma and constant latent heat (Clausius-Clapeyron approximation), no +humidity/mixtures. Correct and sufficient to DRIVE the processes; not spectroscopy / real-gas EoS / CFD. + +NEXT (now unblocked): the material PROCESS layer in the documented order -- M6 material-specific combustion +(autoignition + soot/smoke data, ignite->emitter coupling, ember colour via T3), M5 phase change (latent-heat data + +rule, boil point from T1), M4 oxidization front (reaction-diffusion on HyperCA), M7 burn/decay (mass loss over time, +tying M5+M6). Each is a data layer + coupling onto the fluid/emitter and this heat model -- no new solver. + +## Material processes M6 + M5 (material-specific fire, phase change) -- first two process items, on the thermo foundation (+12 tests, 2273->2285) + +With the thermo trigger layer (T1/T3/T4) in place, built the first two PROCESS items in the documented order. Both +are DATA + COUPLING on the shipped solvers/foundation -- no new solver. PROBE-FIRST: neither existed; the fluid +solver already has GLOBAL combustion (one ignition/burn_rate/smoke_yield), the emitter is generic -- M6's job is to +make them MATERIAL-specific. + +M6 `holographic_combustion.py` -- material-specific combustion, the "wood smoke != plastic smoke" ask. + * COMBUSTION data columns per material: autoignition_K, heat_of_combustion, smoke_color, soot_yield, burn_rate, + flame_temp_K (wood/paper/pvc/abs/gasoline/ethanol/coal/methane -- plausible fire-safety values). + * `ignites(material, T)` -- the honest gate (wood lights at 300C, PVC needs ~450C, nothing at room temp, + non-flammable never). `Fire` object with an IGNITION LATCH: once hot enough it stays lit and consumes fuel at + the material's burn rate until spent, then goes cold (self-sustaining flame; temperature climbs toward + flame_temp_K). `flame_color` = blackbody (T3) at the flame temperature. COUPLINGS: `configure_fluid(fluid, mat)` + sets the StableFluid's global combustion params from the material; `emit_smoke(sdf, mat, ...)` spawns surface + particles coloured by the material's smoke colour + soot. Faculties `fire`, `ignites`. + * Verified: wood/paper smoke pale grey, ethanol near-white (clean), gasoline/pvc/coal dark+sooty + (_combustion_swatch.png -- flame top, smoke bottom). Fixed during build: (a) the fire extinguished itself + (cooling nuked T below autoignition every step) -> added an ignition LATCH + flame temperature so a lit fire + self-sustains, matching real fire; (b) emit_from_surface returns (pos, normals, vel) not (pos, vel). + +M5 `holographic_phase.py` -- phase change with latent heat (water<->steam<->ice), the boiling PLATEAU. + * PHASE_DATA columns: melt_point_K, boil_point_K, latent_fusion, latent_vapor, and per-phase specific heats + (water fully populated; iron/aluminum show it generalises). + * `PhaseState(material, mass, T, pressure)` holds mass across solid/liquid/gas; `.add_heat(Q)` walks energy + through sensible warming (dT=Q/(mc)) and latent transitions, HOLDING temperature flat during melt/boil while + the latent heat is paid -- the textbook plateau. Cooling reverses (condense, freeze). Boiling point tracks + pressure via the gas model (T1 Clausius-Clapeyron) -- boils cooler up a mountain. Faculty `phase_state`. + * Verified: 1 kg water at 99C fed 100 kJ steps HOLDS at 100C for ~2.2 MJ (matching water's 2.257 MJ/kg latent + heat of vaporization) while liquid->steam; melt/freeze hold at 0C; mass conserved across transitions. + +KEPT NEGATIVES (loud): combustion is data-driven PRODUCTS (heat/smoke/soot/rate), not reaction kinetics; flame +spread is left to the fluid solver, not modelled here; numbers are plausible/art-directable, not a combustion-lab +dataset. Phase change is lumped latent-heat BOOKKEEPING with a moving-fraction rule, constant specific/latent heats, +boiling point pressure-dependent but no nucleation/superheating; not molecular dynamics or two-phase CFD. + +REMAINING processes (documented order): M4 oxidization front (reaction-diffusion on the shipped HyperCA -- corrosion +spreads from exposed/wet faces, base->oxide), then M7 burn/decay (object-level consumption over time -- mass loss, +char/ash, tying M5+M6, driven by the heat model). Both still data+coupling, no new solver. + +## Backlog finished (M4 oxidization + M7 burn/decay) + the periodic table of elements (+14 tests, 2285->2299) + +Finished the material-process backlog and added the elemental foundation Moose asked about. + +M4 `holographic_oxidation.py` -- corrosion FRONT (rust/patina). A scalar reaction-diffusion field (the same family +as the shipped HyperCA, kept as a readable oxidation fraction): d_ox/dt = rate*moisture*(seed_rate*exposure + +spread*neighbour_ox)*(1-ox). Corrosion NUCLEATES at exposed/wet faces (default: the grid border) and SPREADS inward +as a front (autocatalytic -- rust feeds rust), monotonic (never un-rusts), saturating. Per-material rate + oxide +colour: steel->rust (orange), copper->patina (green), silver->tarnish, aluminum self-limiting (low rate). `.albedo()` +is the base->oxide BLEND per cell. Faculties `oxidation_field`, `oxide_color`. Verified: front ahead at edges vs +centre, steel>copper, wet>dry, monotonic (_rust_front.png: rust creeping inward over 4 timesteps). + +M7 `holographic_burn.py` -- object BURN/DECAY over time, tying M6. A `BurningObject` drives an M6 Fire, tracks mass +loss (remaining mass = remaining fuel), and marches appearance base->char->ash via a two-stage blend as burn_fraction +climbs; emits the material's smoke; ends as ash; monotonic. `evaporate()` is the M5 analog (a puddle boiling away). +Faculties `burn_object`, `char_color`. Verified: lit wood loses mass to ~0, appearance darkens to char then pales to +ash, emits smoke, ends is_ash; PVC emits black smoke; deterministic. Fix during build: PhaseState init at exactly the +boil point classifies all mass as gas -> start the evaporation puddle just below boiling. + +`holographic_elements.py` -- the PERIODIC TABLE as engine ingredients (Moose's idea; answer to "do we have this?" was +NO, now yes). ~43 curated engine-relevant elements with: atomic mass (real standard atomic weights), density, +melt/boil points, FLAME-TEST colour (Li crimson, Na yellow, K lilac, Ca orange, Sr crimson, Ba/Cu/B green, ...), +category. The COMPOSITION grammar is the point: a material declares elemental makeup + ratio {symbol: count}, and +derived facts fall out by composition, not restatement: + * molar_mass = sum(count*atomic_mass) -- feeds the gas law (T1) + * flame_color_of = the ratio-weighted BLEND of the constituents' flame colours -- feeds combustion (M6); this is + the emission-LINE colour (copper burns green) that the blackbody CONTINUUM alone can't give, closing the honest + gap noted in the blackbody module. + * mass_fractions -- steel is "98% iron by mass" derived from mole counts. +MATERIAL_COMPOSITION links materials down to elements (water H2O, salt NaCl, rust Fe2O3, brass/bronze/steel alloys, +...). Faculties `element`, `material_elemental`. Extensible (add a row / carry on the definition). Verified: +H2O molar mass 18.015, NaCl 58.44; Na flame yellow, Cu green, a 50/50 mix exactly between them, ratio shifts the +blend; salt burns sodium-yellow (_flame_test.png). This is "as above, so below": a material decomposes into elements +the way a VSA record decomposes into role-fillers, and blend is how composites/interpolations are formed. + +KEPT NEGATIVES (loud): oxidation is a phenomenological front (no electrochemistry/galvanic/passivation); burn is +object-level bookkeeping (no resolved char-layer pyrolysis or shrinking geometry -- appearance+mass change, the +SDF/mesh does not physically shrink); elements are a curated ~43-subset (not all 118) with reference values, flame +colours where characteristic ones exist, not a quantum-chemistry engine. All deterministic, NumPy+stdlib, additive. + +The full M1-M7 material backlog is now DONE. Elemental composition opens a follow-up: wire material_elemental's +molar_mass into the gas GAS_PROPERTIES (so T1 derives it from makeup) and its flame_color into the combustion flame +tint (blackbody continuum + element emission lines) -- both small, both now possible. + +## Wrap-up (elements->gas, elements->combustion) + ACOUSTICS A1/A2/A4 (+13 tests, 2299->2312) + +WRAP-UP of the elements work (the two follow-ups): +* Gas molar mass FROM composition: `holographic_gas.molar_mass_of(name)` prefers GAS_PROPERTIES but DERIVES molar + mass from the elemental composition (holographic_elements) for gases not in the table (methane 0.01604 kg/mol + from CH4); specific_gas_constant uses it. Table gases cross-check against composition (CO2 both 0.04401). +* Combustion flame TINT from elements: `flame_color(temp, material=)` blends the blackbody continuum with the + material's elemental emission-LINE colour (copper -> greener flame) -- the line colour the continuum alone can't + give. Fire.step passes its material. Default (material=None) unchanged/backward-compatible. + +ACOUSTICS (new subsystem, from the Acoustics & Cymatics backlog; grounded in the doc's probe -- the key reuse is +that Chladni modes ARE the Laplacian eigenmodes holographic_spectral already computes). Built the first three items: +* A1 `holographic_audio.py` -- read a WAV (stdlib wave; 8/16/32-bit, stereo->mono) -> float samples+rate; + `spectrum`/`dominant_frequencies` (rfft peak-pick: a 440 tone -> single 440 peak, a chord -> its notes); + `frames` (STFT so a changing sound animates a changing pattern). Faculties `read_wav`, `audio_spectrum`. + Kept negative: WAV/PCM only; rectangular window (leakage). +* A2 `holographic_acoustic.py` -- acoustic impedance Z=rho*c (reused density x sound_speed: air ~420, water ~1.48e6, + steel ~4.7e7 rayl); `interface(a,b)` -> (R,T) energy split from the mismatch (air/steel reflects >99.9%, energy + conserved R+T=1); `wall_absorption`/`reflect_absorb` from a new ABSORPTION data column (concrete 0.02 .. acoustic + foam 0.85). The acoustic twin of the light BRDF. Faculties `acoustic_impedance`, `acoustic_interface`. Kept + negative: normal-incidence single-frequency to start. +* A4 `holographic_cymatics.py` -- THE HEADLINE. `ChladniPlate(shape,grid,medium)` builds the Dirichlet 5-point + Laplacian over a square/disk domain, takes its eigenmodes via holographic_spectral.laplacian_eigenbasis (the key + reuse -- eigensolver + sign_fix), tunes mode frequencies to base_hz. `drive(freqs,amps)` resonantly excites modes + near the sound's tones (Lorentzian kernel) -> displacement field; `drive_mode(k)` picks one. Sand (particles) + drifts DOWN grad|u|^2 to the NODES and settles, tracing the figure. Verified: sand |u| (0.117) << plate avg + (0.314) -- sand really sits on the nodal set; square vs circle give different figures; deterministic + (_chladni.png: classic square grids/crosses + circular rings). Faculty `chladni_plate`. Integration test: + a synthesized tone -> audio_spectrum -> plate.drive -> sand settles on nodes, end to end through the mind. + Kept negative: membrane-mode model (Laplacian eigenmodes + node-drift), not the full biharmonic free-edge plate; + dense eigensolver -> modest grid; sand is over-damped drift, not granular collision. + +REMAINING acoustics (documented order): A5 water Faraday + cornstarch media (same ChladniPlate, medium= switch over +the fluid/softbody), A3 scalar wave-equation field (leapfrog + FFT laplacian -- the propagation the incompressible +fluid can't give), A7 acoustic levitation (Gor'kov force on particles in a standing field), A6 geometric room +acoustics (ray tracer + A2 reflectance -> impulse response/reverb). All additive, no new solver category. The doc +notes there is no acoustics seat on the panel -- methods attributed to d'Alembert/Chladni/Faraday/Gor'kov/image- +source + Puckette (audio) + Stam (fluid); an acoustician would be the honest seat to add. + +## Acoustics A5 (water Faraday + cornstarch) + A3 (wave-equation field) (+9 tests, 2312->2321) + +A5 -- the water & cornstarch cymatic media, completing the sand/water/cornstarch trio (added to ChladniPlate as +medium-specific responses over the SAME driving field, no new solver; the incompressible fluid can't do free- +surface so this is the honest phenomenological route the doc allows): +* WATER (Faraday 1831): the standing surface forms at the ANTINODES -- height relaxes toward |u| (crests where the + plate moves most, the opposite of sand-at-nodes). Verified: surface correlates with |u| at ~1.0; cell size + tracks drive frequency (4 antinode cells at a low mode -> 16 at a high one). Rendered as a blue standing surface. +* CORNSTARCH: shear-thickening -- local shear rate ~ drive_hz * |u|; above a threshold it thickens and HOLDS a + peak, below it relaxes to flat. Verified: peaks held under fast drive (base_hz 1400) but slumped under slow + (base_hz 40). Rendered as pale standing fingers. `step_medium` branches on medium; `render` too. (_cymatics_media + .png: sand | water | cornstarch on one mode.) Kept negative: standing-pattern phenomenology (crests at antinodes, + Faraday half-frequency noted), not a free-surface two-phase solve; cornstarch is a shear-thickening term, not + suspension mechanics. + +A3 `holographic_wave.py` -- the scalar acoustic WAVE field (the propagation the incompressible fluid cannot carry). +d2p/dt2 = c^2 grad^2 p by explicit LEAPFROG with a finite-difference Laplacian (readable, per preference): the whole +solver is p_next = 2p - p_prev + (c*dt/dx)^2 * lap(p). `c` scalar or per-cell field (from material sound_speed). +`pulse`/`source`, a sponge `absorb_border`, and CFL AUTO-SUBSTEP (dt <= dx/(c*sqrt(ndim)); a big dt is split into +stable inner steps -- stated loud, not hidden). Verified: a 1-D pulse splits into two movers each at c (d'Alembert); +energy stays bounded even for a huge requested dt (the CFL guard); front distance scales with c; an absorbing border +soaks the wave a hard wall keeps; deterministic. Faculty `wave_field`. Kept negative: scalar linear acoustics (no +elastic/vector waves, no shocks); sponge is a crude PML. + +Acoustics now: A1 (audio) + A2 (impedance) + A4 (Chladni sand) + A5 (water/cornstarch) + A3 (wave field) done. +REMAINING: A7 acoustic levitation (Gor'kov radiation force on particles in a standing wave -- now buildable, since +A3 gives the standing field; beads to pressure nodes against gravity), then A6 geometric room acoustics (ray tracer ++ A2 reflectance/absorption -> impulse response / reverb). Both additive, no new solver category. + +## Non-Newtonian fluid: real cornstarch (power-law viscosity) (+7 tests, 2321->2328) + +Moose flagged that the A5 cymatics cornstarch is a plate-SURFACE phenomenology, and asked whether we can actually +SIMULATE cornstarch -- i.e. give the FLUID solver non-Newtonian rheology. Built it from Moose's power-law snippet. + +`holographic_nonnewtonian.py` -- the Ostwald-de Waele power law: eta = K * shear_rate^(n-1), clamped [eta_min, +eta_max]. n>1 = DILATANT/shear-thickening (cornstarch/oobleck: stiffens the harder you shear); n<1 = shear-thinning +(ketchup/paint/blood); n=1 = Newtonian constant. The viscosity is now a FIELD, one value per cell. Provides: +* `power_law_viscosity(shear, K, n)` -- the law (Moose's snippet). +* `strain_rate_tensor` / `strain_rate_magnitude(vel, dx)` -- gamma_dot = sqrt(2 e:e), the shear rate the law reads + (0 for a uniform flow, constant for a simple shear). +* `viscous_step(vel, K, n, dt, dx)` -- one variable-viscosity update dv = dt*(1/rho)*div(eta*(grad v + grad v^T)), + tau_ij=2 eta e_ij, finite-difference div; AUTO-SUBSTEPS for stability when eta spikes. Returns (vel, eta field). +Verified: eta rises ~19x from slow to fast shear at n=1.8 (thickens), falls for n<1, flat for n=1, clamped; a +curved (sinusoidal) shear is damped MORE by cornstarch than by water (a linear shear has no curvature to diffuse -- +that was a kept gotcha); eta is a field, not a constant; deterministic. + +Wired into `holographic_fluid.StableFluid` as an OPT-IN mode: new `power_law_n`/`consistency_K` params; when +power_law_n != 1 (and 2-D) the step replaces the constant-viscosity Fourier diffuse with the power-law viscous_step. +n=1 (default) is BYTE-IDENTICAL to the old solver (verified: same density+vel after steps). Fluid-level signature: +a cornstarch fluid keeps less sheared-flow energy after a step than a Newtonian one (0.978 vs 0.999) -- it resists +the shear more. Faculties `power_law_viscosity`, `nonnewtonian_fluid`. Injecting a div-free shear needs the velocity +component ALONG one axis varying ACROSS the other (else the projection zeroes it -- a kept gotcha from the solver's +incompressibility). + +KEPT NEGATIVES (loud): power-law (Ostwald-de Waele) model -- no yield stress (Herschel-Bulkley) and no time- +dependence (thixotropy); clamp keeps eta finite (real oobleck stress can run away); 2-D, unit density; the +non-Newtonian branch runs on host arrays (CPU). The deep rheology is cleanest at the viscous_step level; inside the +full incompressible solver the projection/advection partly mask it (measured at one step before they wash it out). + +Acoustics remaining: A7 levitation (Gor'kov force in a standing wave), A6 geometric room acoustics (ray tracer + +A2). Also lightened the A4/A5/A3 tour demo blocks (grid 40->24, fewer modes/steps) after a tour run was resource- +killed mid-way -- tour demos are illustrations; the real verification is the pytest suite. + +## Acoustics A7 (levitation) + A6 (room acoustics) -- the acoustics backlog is DONE (+8 tests, 2328->2336) + +A7 `holographic_levitate.py` -- ACOUSTIC LEVITATION, the "sound moves objects" showpiece. In a vertical standing +wave p=A*cos(k*y), the Gor'kov (1962) radiation force F = -grad(Gorkov potential) pushes small dense particles to +the pressure NODES (spaced lambda/2) and holds them against gravity. `gorkov_potential`/`gorkov_force_y` (force via +central difference of U -- readable, no hand-differentiated trig), `_scattering_factors` (f1 monopole, f2 dipole +from density/compressibility contrast), `pressure_nodes`, and `LevitationChamber` (beads = ParticleSystem, force = +Gor'kov + gravity, air-drag damping). Verified: force ~0 at a node and points toward it from either side (stable +trap); FIELD ON holds 100% of dense beads aloft near the nodes; FIELD OFF they fall to the floor; node spacing +lambda/2; deterministic (_levitation.png). Faculty `levitation_chamber`. Fix during build: initial per-step damping +was unphysically heavy so beads fell at a crawl -> lightened so the field-off fall completes. Kept negative: Gor'kov +holds for particles << wavelength (Rayleigh limit); 1-D vertical wave; inviscid; no acoustic streaming. + +A6 `holographic_roomacoustic.py` -- GEOMETRIC ROOM ACOUSTICS, the last item. `ShoeboxRoom`: early reflections via +the IMAGE-SOURCE method (Allen & Berkley 1979 -- mirror the source across each wall; each image is a tap at delay = +path/c, amplitude = product(wall reflectance)/path), and reverberation time via SABINE (RT60 = 0.161 V / (alpha* +area)). Reuses A2 for wall reflectance = sqrt(1-alpha) and named-material absorption. Verified: direct sound at +|s-r|/c; reflections arrive later at geometrically-correct delays (floor bounce 14.6 ms); RT60 live 3.58s >> dead +0.24s (drops as absorption rises); a livelier room reflects more energy back; concrete rings longer than carpet; +deterministic (_reverb.png). Faculty `room_acoustics`. Fix during build: the RIR-tail energy metric was confounded +by the two rooms having different RIR array lengths (longer RT60 -> longer array) -> compare reflected-energy from +the taps directly instead. Kept negative: geometric acoustics is a HIGH-frequency approximation (no diffraction/ +interference -- the A3 wave field is the low-frequency complement); early reflections to a low image-source order +(exact for a shoebox), the late tail summarised by Sabine; rectangular room. + +THE ACOUSTICS & CYMATICS BACKLOG IS COMPLETE: A1 audio, A2 impedance, A3 wave field, A4 Chladni sand, A5 water/ +cornstarch, A6 room acoustics, A7 levitation -- plus the real non-Newtonian cornstarch rheology. All additive, no +new solver category, each reusing what was already in the engine (spectral eigenmodes, sound_speed/density data, +WAV I/O, particle system, ray/impedance). The doc's note stands: there is no acoustics SEAT on the panel -- work +attributed to d'Alembert/Chladni/Faraday/Gor'kov/Allen-Berkley/Sabine + Puckette (audio) + Stam (fluid); adding an +acoustician would be the honest next seat. + +## SIGGRAPH list #1 curl noise + #2 tearing -- starting down the scouting list (+10 tests, 2336->2346) + +Two new SIGGRAPH-scouting-list items, the ones the report said to pick up first. + +#1 `holographic_curlnoise.py` -- CURL NOISE, the cheapest real win. Divergence-free procedural turbulence = +curl of an fBm streamfunction: velocity (u,v) = (dpsi/dy, -dpsi/dx), divergence-free because mixed partials +commute (DISCRETELY too for matching central differences -> divergence-free to MACHINE precision, measured +max|div|=0.0). Reuses holographic_noise.FractalNoise for the potential; np.gradient for the curl. Boundary +improvement (Bridson): ramp the streamfunction to 0 at an obstacle (smoothstep of its SDF) so the surface is a +streamline and the flow goes AROUND it -- measured inside-obstacle speed 0% of outside, still divergence-free. +Also curl_noise_3d (curl of a vector potential). Faculty `curl_noise`. Cheap wind/smoke detail with no fluid +solve; seeds the fluid solver, the acoustics wind. Kept negative: 2-D streamfunction (3-D takes a vector +potential); pins streamfunction = no-penetration but not no-slip (tangential flow unconstrained -- the standard +curl-noise trade-off). Visual _curlnoise.png (flow streaking around a disk). + +#2 `holographic_tear.py` -- TEARING a thin sheet, a genuine NEW capability (the engine had no fracture). A +breakable-constraint cloth on the PBD softbody: each distance link has a tear strength (max strain); after each +step, links stretched past it SNAP (dropped from the constraint list); the crack propagates because a broken +link's neighbours then carry more load and reach their own tear strain -- the reference method's physics +(Pfaff/Narain/O'Brien) expressed on the constraint graph instead of by remeshing. `TearableCloth` wraps +SoftBody.cloth; `_tear()` snaps links; `connected_components()`/`piece_sizes()` (union-find over surviving +links) report how many PIECES the sheet split into. New material data column TEAR_STRENGTH {paper 0.06, +wet_paper 0.03, foil, cotton, denim, leather, knit, rubber 0.80} = strain tolerated before snapping. Measured: +a yanked paper sheet snapped ~90 links and split into 3 pieces; rubber (high tear strain) tore ~34 under the +same pull; a gentle load tore nothing; deterministic. Faculty `tearable_cloth`. Visual _tearing.png (intact vs +ripped). Kept negative: a mass-spring/breakable-constraint tear (readable baseline), NOT adaptive remeshing -- +the crack follows existing grid links so it is as sharp as the mesh resolution and torn edges are not +re-triangulated; sits on XPBD compliance so the sheet can stretch to reach the tear strain. Why not half-edge +surgery: the mesh kernel exposes no split-and-separate/vertex-duplication op, and the constraint-graph tear is +the readable on-substrate route. + +Scouting-list status: #1, #2 done. The report's headline unlock is #7/M1 WALK-ON-SPHERES (grid-free Monte +Carlo PDE solver on the SDF) -- the audit's #1 "act on this," and the missing steady/elliptic solver under the +thermo heat model + acoustics (the steady complement to the A3 wave field). That is the natural next big one; +curl noise (#1) was the cheap win, tearing (#2) the asked-for capability. Tests are compute-heavy for tearing +(many PBD steps x constraint loops ~58s) -- kept the tour demo LIGHT accordingly. + +## SIGGRAPH list #7 -- Walk on Spheres: grid-free Monte-Carlo PDE solver (+7 tests, 2346->2353) + +The scouting list's headline unlock and the audit's #1 "act on this". `holographic_wos.py` -- Walk on Spheres +(Sawhney & Crane 2020): solve Laplace (Delta u=0, Dirichlet) or Poisson (-Delta u=f) at a point by random walks +that, each step, jump to a uniform random point on the largest empty sphere -- whose radius is the distance to +the boundary, i.e. ONE SDF EVALUATION -- until they reach the boundary and read the boundary value there; +average many walks. Mesh-free, pointwise (solve only where you look), embarrassingly parallel. The random-walk- +and-average pattern is the path tracer pointed at a PDE instead of light. + +Functions: `walk_on_spheres(points, dist_to_boundary, boundary_value, source=None, n_walks, eps, max_steps, +seed)` (the vectorized core -- all query points x all walkers stepped together, masked as they hit the +boundary); `solve_on_sdf(sdf, boundary_value, points, source=None, ...)` (distance = -sdf.eval, the whole reason +it fits an SDF engine). Poisson uses a single-sample source estimator with the ball's harmonic Green's function +(2D (1/2pi)ln(R/s); 3D (1/4pi)(1/s-1/R)) and ball volume. Reports (mean, standard_error) -- the MC noise is on +the record (the `measure` discipline). + +Verified against closed forms: constant boundary -> constant (exact); a harmonic (linear) boundary g=x -> +interior u=x within MC error; the ANNULUS Laplace log-profile u(r)=ln(r/r_in)/ln(r_out/r_in) = 0.585 at r=1.5 +(matched); Poisson -Delta u=1 on a disk, u=0 on boundary -> u(0)=R^2/4 (2D) matched, and R^2/6 in 3D (dimension +matters -- caught in the integration test); standard error shrinks ~1/sqrt(N) (16x walks -> ~4x tighter); +deterministic given seed. Faculties `solve_pde` (general Laplace/Poisson on an SDF) and `steady_heat` (named for +the heat use: hold the boundary temperature, find the equilibrium interior temperature). Visual _wos_heat.png: +steady heat from a hot disk out to cold square walls, solved grid-free at ~2000 interior points. + +This is the STEADY/elliptic complement to the transient `holographic_heat` diffusion and the `holographic_wave` +acoustic field -- the missing solver under the thermo heat model and the acoustics backlog, now mesh-free on any +SDF (sidesteps the Python-loop-bound mesh kernel for this whole class). KEPT NEGATIVES (loud): Monte Carlo, so +noisy, converges as 1/sqrt(N) (the error bars cut both ways); pure Dirichlet (reflecting/Neumann needs Walk on +Stars); elliptic/parabolic problems (Laplace/Poisson/steady heat), not everything; the source estimator is +single-sample per step (variance-reduction e.g. gradient-domain reconstruction is the polish, not built). + +Scouting-list status: #1 curl noise, #2 tearing, #7 Walk on Spheres done. Per the audit, the reusable extensions +(distribute for parallel walks, dirtyfield for incremental re-solve near edits, measure for CIs, tree/pivot as +closest-point accelerators) are the natural follow-ups but each is a separate measured build. Next: the Hair & +Fur backlog (grooming + PBD strands + a fiber shader; curl noise already feeds its H7 wind). + +## Hair & Fur backlog: H1-H7 (groom + PBD strands + guide interpolation + curl-noise wind + fiber shading) (+11 tests, 2353->2364) + +Knocked out the Hair & Fur backlog. The audit's finding held: the dynamics substrate was already here, so this +is a groom LAYER plus a fiber shader, reusing emitter/subdivcurve/softbody/collide/curlnoise -- "add a data +layer and a coupling" once more. Two new modules. + +`holographic_groom.py` (H1/H2/H3/H7): +* H1 -- `Strand` (points root->tip, root normal, width, attrs; .length/.tangents/.smoothed via subdivcurve) and + `groom(surface_sdf, n, bounds, length, curl, lean, ...)`: roots placed by emit_from_surface (with outward + normals), each strand grown along its normal (+optional lean), straight or a tapered HELIX for curls. NOTE the + bounds are (lo_vec, hi_vec) -- emit_from_surface reads D from lo.shape, so a per-axis [(-a,a)]*3 list is + misread as 2-D (a gotcha, fixed). +* H2 -- a strand IS a rope with a pinned root: `build_strand_body` = SoftBody with w[0]=0 (pinned root), + distance constraints (inextensible) + bend springs (i,i+2, Provot). `follow_the_leader` (Muller 2012) pass for + stiff inextensibility. `simulate_strands(...)` steps under gravity + optional wind force + body-SDF collision. + Verified: a pinned strand falls under gravity, keeps its length (FTL), root stays put. +* H3 -- `interpolate_strands(guides, render_roots, k, clump)`: blend the k nearest guides' root-relative shapes + by inverse distance, then clump toward the nearest -- many render strands from few guides. Brute nearest + (readable; tree is the sublinear accelerator if it grows). +* H7 -- `CurlWind`: a divergence-free 3-D curl-noise wind (reuses holographic_curlnoise) sampled per strand + point as a force; verified it moves hair WITHOUT ballooning (curl noise can't compress). + +`holographic_hairshade.py` (H4/H5/H6): +* H4 -- `kajiya_kay(tangent, light, view, ...)`: diffuse = sin(T,L), specular = cos(theta_L - theta_V)^n along + the tangent -- the lengthwise sheen a normal-based shader can't make. Verified anisotropic. +* H5 -- `marschner(...)`: compact single-scattering R/TT/TRT fiber BSDF -- longitudinal Gaussians of the + half-angle at the canonical shifts/widths, simple azimuthal lobes, absorption from hair color + (`absorption_from_color` = -ln color). Verified: blonde brighter than black; the TRT secondary highlight is + present and COLORED (R is white); energy loosely bounded. `marschner_lobes` returns R/TT/TRT separately. +* H6 -- `render_hair(strands, camera, shader, lod_stride, ...)`: project the smoothed centerline (camera + view+projection), shade each segment by its tangent, draw far->near. Verified a 200-strand groom renders to a + PNG and a coarser LOD keeps the silhouette (overlap ~100%). Visual _fur.png (Kajiya-Kay brown vs Marschner + blonde furred sphere, drooped under gravity + curl-noise breeze). + +Faculties: groom_hair, simulate_hair, interpolate_hair, hair_wind, render_hair. + +KEPT NEGATIVES (loud): H1 is the REST groom, procedural attributes not a scan. H2 gives bend, not TWIST -- +curls hold via bend-spring rest curvature, but true torsion needs the Cosserat/orientation-frame upgrade (H2b), +DELIBERATELY DEFERRED as the heavy optional rung (the doc defers it too). H3 interpolation is a believable +approximation, not per-strand physics; full strand counts + hair-hair collision are Python-loop-bound (guide +interpolation is the mitigation). H5 Marschner is an approximation (no full Bravais/Fresnel/caustics), single +scattering only -- multiple scattering / dual scattering (the soft glow of blonde) is the harder further rung. +Rendering is opaque, depth-ordered; order-independent transparency for dense overlap is left for later. + +SIGGRAPH scouting list so far: #1 curl noise, #2 tearing, #7 Walk-on-Spheres. Hair & Fur backlog: H1-H7 done, +H2b (twist) deferred. Moose has another backlog queued next. + +## Hair H2b (Cosserat twist) + Mesh kernel performance fix (+11 tests, 2364->2375) + +Two things: finished the deferred hair rung, then took the mesh performance backlog. + +### H2b -- twist via a Cosserat rod (`holographic_cosserat.py`) +The quality upgrade over plain bend springs (H2): each segment carries an ORIENTATION FRAME (a quaternion), so +the strand HOLDS its curl under gravity and can carry a TWIST (Kugelstadt-Schomer 2016, the PBD route to +Discrete Elastic Rods). Small readable quaternion helpers (qmul/qconj/quat_rotate/quat_between/axis_angle) since +the engine had none. `CosseratStrand`: rest frames built by parallel transport, rest Darboux = relative rotation +between consecutive rest frames (stores curl+twist); each step = predict -> FTL inextensibility -> align frames +to edges (preserving roll) -> relax bend-twist toward rest -> reconstruct centerline from frames (curl feedback) +-> final FTL. The curl memory is TENSION-AWARE: reconstruction weight fades to 0 as the strand nears full +extension, so pulling it taut UN-CURLS it (physical). Verified: frames hold the curl under gravity (0.29 vs +rest 0.30; a plain chain drifts to 0.34); stretching taut drops curl to <0.5*rest; a root twist propagates down +the frames (twist DOF a bend model lacks); inextensible to 1e-6; deterministic. Faculty `cosserat_strand`. +KEPT NEGATIVE: the roll/twist is carried and coupled but the visible CENTERLINE follows the tangent (roll matters +for an oriented cross-section, e.g. a ribbon, not a round fibre); positions reconstructed from frames each step +(FTL-with-orientations) is the simplified coupling, not the full simultaneous stretch-shear solve; opt-in +(frames cost more + a sign/tie convention). The Hair & Fur backlog is now COMPLETE (H1-H7 + H2b). + +### Mesh kernel performance fix (`holographic_mesh.py`) -- the Python-loop-bound negative, addressed +From the implementation spec. The mesh was slow because the REGULAR parts (adjacency build, neighbourhood +queries) were Python loops; they vectorize to integer NumPy that is bit-for-bit identical. +* CHANGE 1 (headline): `half_edges()` now dispatches -- TRIANGLE meshes take `_half_edges_tri` (vectorized: + h=3*face+corner; twins matched by sorting a packed (min,max) undirected-edge key, no dict), polygon meshes + fall back to `_half_edges_loop` (the original, kept as the reference). Both produce BYTE-IDENTICAL tables + (origin/face/nxt/twin). Non-manifold (a repeated DIRECTED edge) raises the SAME error in both paths; boundary + twins stay -1. Measured: ~4x on a small grid, and per the spec it WIDENS with size (3x @ 40x40 -> 35x @ + 120x120) because the dict loop is O(H) Python and the sort is one vectorized pass. int64 edge key with an + overflow assert (vertex_count^2 < 2^62). +* CHANGE 2: `_adjacency()` builds a one-time CSR index (half-edges grouped by origin via one stable argsort); + `vertex_faces`/`vertex_neighbours` are now O(deg v) slices instead of O(V*H) scans, with IDENTICAL sorted + output. `_adj` cache invalidated wherever `_he` is. +Pinned byte-for-byte: test_holographic_mesh asserts the tri build == loop build element-for-element on box/grid/ +tetra, boundary twins reciprocal, non-manifold raises in both paths, and CSR queries == the old full scan for +every vertex. Broad regression: 409 tests across the whole mesh family (verbs/subdiv/ik/qem/curvature/geodesic/ +uv/gltf/bridge) + integration all green -- no downstream decision shifted (the bind_batch discipline). +KEPT NEGATIVES (loud, per the spec): n-gon (polygon) meshes still use the loop (only triangles are vectorized); +the ORDER-DEPENDENT operators (greedy QEM, one-at-a-time Euler edits) are deliberately LEFT scalar -- naive +vectorization there was measured slower and not bit-exact; the honest tool for those is the opt-in numba path +(Change 4), not vectorization. Change 3 (loop-subdivision-as-matmul) not done this pass -- the two bit-exact +wins (build + queries) are the headline; subdivision-as-matmul and the numba path remain as noted follow-ups. + +Backlog status: SIGGRAPH list #1/#2/#7 done; Hair & Fur H1-H7 + H2b done; mesh perf fix Changes 1-2 done +(3-4 noted). + +## Mesh perf fix Changes 3 & 4 -- finishing the backlog (+2 tests) + +Finished the remaining two changes from the mesh performance spec. + +CHANGE 3 -- loop subdivision as a MATRIX (`holographic_meshsubdiv.py`). Subdivision is REGULAR, so the new +positions are a fixed linear map of the old ones: new_V = S @ V. `_subdivision_operator` builds S once from the +connectivity as scipy-free (rows, cols, weights) index/weight arrays (row layout matching _one_level exactly: +rows 0..nV-1 repositioned old vertices, rows nV.. new edge vertices in sorted-edge order), plus the refined face +list. `_one_level_matrix` applies it with ONE weighted scatter-add (np.add.at) -- the per-vertex/per-edge Python +arithmetic (esp. `sum(V[u] for u in ring)`) becomes a single vectorized matvec. `loop_subdivide` now uses it; +`_one_level` stays as the readable reference. BIT-EXACT: box/tetra positions match the loop to 0.0, grid to one +ULP (5.6e-17); topology EXACT; multi-level + Euler chi preserved. Pinned in test_holographic_meshsubdiv. Kept +negative: same TOL caveat as any reordered float sum (weights identical, summation order differs by ULPs). + +CHANGE 4 -- the order-dependent operators, resolved per the spec (NOT vectorized -- that would be slower and not +bit-exact). The vectorized alternative already exists: `meshqem.cluster_decimate(mesh, grid)` (vertex clustering, +one vectorized pass) is the fast route when exact greedy collapse-order is not required. Greedy `qem_decimate` +stays SCALAR and bit-exact (order-dependent: a priority heap where each collapse depends on the last; ULPs in the +quadric sums flip collapse order, so vertex_quadrics stays exact-scalar too). The numba route is available opt-in +via holographic_jit for the profiled case, but is NOT force-applied: the greedy loop mutates the mesh through the +guarded collapse_edge (link-condition refusal), so it is not a clean njit target, and the honest default is to +leave it scalar. So Change 4 is complete as a POLICY: clustering = the vectorized decimator (shipped), greedy QEM += scalar + bit-exact (correct), numba = opt-in where a profile demands it. + +Mesh performance backlog COMPLETE: Change 1 (vectorized triangle half-edge build, 3-35x, bit-exact), Change 2 +(CSR adjacency -> O(deg) queries), Change 3 (subdivision-as-matmul, bit-exact), Change 4 (clustering exists; +greedy stays scalar; numba opt-in). The regular parts are no longer Python-loop bound; the irreducibly-sequential +parts stay scalar by design, with clustering + numba as the honest escape hatches. + +## Compute Architecture backlog -- the GPU-shaped gaps NumPy leaves, filled VSA-natively (+18 tests, 2377->2395) + +Built the whole Compute Architecture plan: the scheduler rung between the program layer and the kernel, plus the +three primitives it orchestrates. Built in the plan's Step-0-re-ordered sequence (fusion first, it's the win). + +FILL 2 -- SPECTRAL FUSION (`holographic_fuse.py`), the keystone. bind=multiply, bundle=add, permute=phase-ramp, +unbind=multiply-by-conjugate are ALL linear in spectral coordinates, so a straight-line chain is ONE expression +in Fourier space: one rfft per distinct leaf, all algebra on spectra, one irfft out -- instead of op-by-op +irfft->rfft round-trips between every link. A tiny five-op expression tree (Leaf/Bind/Unbind/Bundle/Permute, +NOT a general autograd graph -- deliberate readable scope). bundle's normalization handled by one irfft-for-norm +per bundle node (the summed vector's L2 norm), so it stays correct mid-chain. MEASURED: a K-bind accumulation +chain does <=K+2 FFTs vs 3K op-by-op, ~2.2x wall on a 16-bind chain (the plan's 2.0-2.6x). Matches op-by-op to +<1e-10 across all five ops + the keystone chain + the record pattern (== bundle_bind). KEPT NEGATIVE: tolerance- +not-bit-exact (~1e-15, reordered float adds) so it stays OFF the tie-sensitive paths (the bind_batch maze lesson); +cleanup/cosine/argmax are boundaries fusion can't cross; a no-op on compute-bound (~0% FFT) work. + +FILL 1 -- SPECTRUM RESIDENCY (`holographic_residency.py`), the cheap bit-exact companion. SpectrumCache: content- +hashed (sha256, not python hash()) LRU cache of rfft(atom), so binds/unbinds against KNOWN atoms skip the forward +transform. `bind_cached`/`unbind_cached` BIT-IDENTICAL to the kernel (<1e-12 -- it IS the same rfft). Honest per +the plan's own tempering: ~1.4x standalone and overlaps bind_fixed; its real value is INSIDE fusion (makes a +fused chain's leaf-transforms free for known atoms). Changed atom hashes differently -> invalidation is free. + +FILL 3 -- AUTO-SUPERPOSITION + SPILL (`holographic_superschedule.py`), the latency-hiding move. pack_capacity is +the bucket-sizing DIAL (0.10*D gated / 0.02*D continuous), NOT a wall (the rev-3 stance correction). superpose_batch +packs up to the dial into one vector and SPILLS the overflow across buckets rather than abstaining; apply_in_ +superposition binds each bucket's whole bundle by one op at once (N transforms in flight from a handful of packed +ops). MEASURED @ D=512: gated recall 1.00 under the dial (cap=51); over it, SPILL beats cram 0.69 vs 0.41 -- the +whole point of not abstaining; one bind transforms all items (fid 0.42). KEPT NEGATIVE: the dial is PRICED +(widening D is sublinear-gain/linear-cost); the continuous dial is much smaller (made visible); nonlinear-feedback +batches genuinely don't superpose (distribute's negative). + +FILL 4 -- THE PROGRAM SCHEDULER / cost model (`holographic_schedule.py`), the capstone, built on 1-3. A VSA program +is a DAG of Ops (leaf/bind/unbind/bundle/permute/cleanup, tie_sensitive flag). `plan` marks FUSE-ROOTS -- the top +of a maximal linear, single-consumer, non-tie-sensitive run at least min_run long (the roofline cost gate) that +heads an output/cleanup/shared node. `run_scheduled` fuses each such run via Fill 2, keeps tie-sensitive runs +op-by-op & BIT-EXACT, and crosses to Python ONLY at cleanups; `run_sequential` is the op-by-op baseline. MEASURED +on a compose+recall pipeline (3 binds + bundle + unbind + cleanup): scheduled 8 FFTs / 1 kernel-call vs sequential +12 / 5, same cleanup winner, pre-cleanup vector matches to <1e-9. Tie-sensitive run stays bit-exact & unfused +(plan makes no fuse-root of it). `plan_signature` is a deterministic sha256 of DAG-shape+plan -> a recurring shape +reuses its schedule (the amortization). KEPT NEGATIVE: reduces the COUNT of crossings, doesn't zero them (the final +commit is a real collapse); no-op speed-wise on compute-bound runs (just avoids materialization). + +Six faculties wired default-off & delegating: spectrum_cache, fuse_record, fuse_expression, superpose_batch, +apply_in_superposition, schedule_program. NON-GOALS kept loud (NOT built): async thread scheduler; CuPy-by-default; +general trace-JIT/autograd; pyFFTW at operating dims; the SER-native scheduler variant as DEFAULT (it's a fork to +be measured against the plain scheduler -- its coherence-key-is-a-hypervector / grouping-is-a-cleanup idea is +noted, but the reorder cost only pays above some DAG size, so the plain scheduler ships first). + +## Scheduler integration -- wiring Fill 2/4 into the real Layer-4 program representation (+3 tests, 2395->2398) + +The four compute-architecture modules shipped with faculties + integration tests last pass, but the scheduler +was only proven on SYNTHETIC programs. This pass integrates it into the real program layer so it isn't a siloed +drawer -- the development-guide rule that a faculty which only imports is still a silo. + +THE INTEGRATION -- the recipe->scheduler bridge (`holographic_schedule.from_recipe` / `run_recipe`; faculty +`realize_recipe_fused`). A StructureRecipe (Layer 4) is ALREADY a DAG of atom/raw/bind/bundle/superpose/permute/ +normalize ops over handles -- exactly the shape the scheduler consumes -- and it has NO cleanups, so it is the +long straight-line bind/bundle run the plan named as the prime fusion target. `from_recipe` lowers a recipe 1:1 +into scheduler Ops (atoms materialized from the seed exactly as build() does); `run_recipe` fuses the linear runs. +Extended the fuser + scheduler with the two recipe ops they lacked: a plain `Sum` node (superpose = spectral add +with NO renormalization, still linear/fusable) and `NORMALIZE` (a boundary -- materialize + divide by norm, no +FFT). Also FIXED the fuse-root detector: a linear node now heads a run whenever any consumer is non-linear +(cleanup OR normalize OR any boundary) or tie-sensitive or shared -- previously it only triggered on cleanups, so +a record feeding a normalize never fused. MEASURED: a role/filler record recipe (4 binds + bundle + permute + +normalize) fuses to 10 FFTs / 2 kernel-calls vs 12 / 7 op-by-op, matching the exact build() to 6e-17. + +KEPT NEGATIVE / boundary, loud: `recipe.build()` advertises BIT-EXACT replay; fusion is ~1e-15 (reordered float +adds), so `run_recipe`/`realize_recipe_fused` is an OPT-IN THROUGHPUT path that does NOT preserve bit-exactness -- +build()/realize stay the exact default. Recipes using `repeat` templates are NOT lowered (their template ops use +local indices); run_recipe falls back to the exact op-by-op build for those (stats['fused']=False), so it is +always correct, just not always fused. + +MACHINE (Layer 3) -- deliberately NOT fused, and this is the honest call, not an omission. `machine._read`/decode +is `unbind(program, pos)` then a CLEANUP for the opcode then a second CLEANUP for the operand -- a chain of +DECISIONS (argmax) that must be exact and are inherently sequential (you must decode the opcode to know which +codebook to clean the operand against). Fusion cannot cross a cleanup, so the decode stays op-by-op on the frozen +kernel (correct). The safe future speed-up there is spectrum RESIDENCY (Fill 1, bit-exact) on the fixed decode +atoms (self.OP/ARG/pos, program_vec) -- noted, not forced (a modest ~1.4x change to a core module, earns its +place only if the machine profiles as hot). So: scheduler integrated where fusion is SAFE (Layer 4 recipes, +wide-margin, no cleanups); left op-by-op where the path is decision-bounded (the machine decode). That is the +plan's own tie-sensitive discipline applied to integration. + +## Scheduler work: integrated where needed -- machine decode residency (+1 test) + +Audited whether the Compute Architecture work is fully implemented and USED, not just callable. Findings: +* Fills 1-4 + the Layer-4 recipe integration (`run_recipe` / `realize_recipe_fused`) were already built and pass + (verified: 23 tests green). The recipe path fuses a real StructureRecipe's straight-line runs vs op-by-op build. +* The one NAMED integration point still raw was `holographic_machine.run` -- the plan's own observation that the + VM "recomputes rfft(program_vec) on every opcode decode." Now fixed: `run()` and `run_batch()` transform the + CONSTANT program vector ONCE per call (`prog_spec = rfft(program_vec)`) and read every instruction address via + `_read_addr(prog_spec, i, n)`, which is BIT-IDENTICAL to `unbind(program_vec, pos(i))` (same rfft(program_vec), + same involution, same irfft). Bit-exactness is the safety argument: the decode is cleanup-gated (a _nearest + winner), and only a bit-exact change is guaranteed not to flip it (the bind_batch discipline). Pinned by + test_residency_decode_bit_exact (exact array-equality, not tolerance); all 22 machine tests + 76 machine- + dependent tests (typed/isa_callstack/isa_registers/protocol/procedure/lexicon) stay green, proving no decode + decision shifted. This is Fill 1 (residency) applied to the hottest loop in the VM: one program transform per + run instead of one per instruction. +HONEST remaining step (kept loud, NOT done): converting run()'s interleaved decode-then-apply loop into a fully +FUSED scheduled executor (fuse the straight-line VALUE run between decodes) needs decode-ahead, because the +per-instruction decode cleanups are genuine fusion boundaries -- that is the capstone-level rewrite. The scheduler +mechanism for it already ships as `schedule_program` (DAG form) and `run_recipe` (Layer 4); the VM's own loop uses +the bit-exact residency win today and stays op-by-op-safe on its tie-sensitive decode. + +## Above/Below Sweep 3 -- the wide-reach five wired (+14 tests) + +Ran the sweep's re-prioritized plan: do the five wide-fanout mechanisms first (each pays off in several places), +probe-first (four of five already EXISTED; the job was wiring the orphans + building the two genuinely missing). + +ITEM 1 -- FACULTY->HANDLER BRIDGE (capability table). The registration side already shipped +(`register_apply_handler(name, fn)` makes any acc->acc faculty callable as `APPLY `). Added the READ side +the sweep flagged: `faculties()` returns the sorted names of every APPLY-able faculty (built-ins + registered), +reading the SAME live handler set the machine's APPLY uses -- so introspection / a drives scheduler / an moe gate +all read one registry (the convergence point). Verified: base table lists cleanup/denoise; a registered `negate` +appears; a program can APPLY it. + +ITEM 2 -- ONE SHARED SPATIAL INDEX (`holographic_spatial.py`, NEW). A uniform-grid index: radius / knn / closest +by scanning only nearby cells, BYTE-IDENTICAL to a brute-force scan (same set, same order; ties by index). The +widest-fanout item (cull/nav/collision/sampling/WoS all ask "what's near here?"). Pinned bit-exact vs brute force +in 2-D and 3-D, uniform and clustered, empty-safe, deterministic. Faculty `spatial_index`. HONEST: uniform grid +is best for evenly-spread points (clustered -> prefer octree, the hierarchical sibling); the 14-consumer MIGRATION +is deferred per the sweep's own hook (prove against the brute-force query first, migrate later). + +ITEM 3 -- `automaton` WIRED (was orphaned, 0 refs). Faculty `reaction_diffusion(size,dim,steps,seed)` runs the +HyperCA (a local rule over a hypervector field -> emergent spots/stripes/fronts) -- one solver, six content +domains (patina, texture, fur/skin, crystal, erosion). Tests: stays finite & unit-normalized, a pattern emerges +(moves off the initial state), deterministic. + +ITEM 4 -- `emergence` WIRED (was orphaned). Faculty `emergent_concepts(...)` -- online, label-free concept growth +(an online GROUP BY); commits a concept via the double-diffusion staircase, which naturally pulls the diffusion +mechanism in with it. Tests: a tight cluster forms a concept, two separated clusters form two (discovered, not +labelled), deterministic. + +ITEM 5 -- TEMPORAL-REUSE LOOP (`holographic_temporal.py`, NEW, on top of backwardwarp). The sweep's point: the +LOOP is the work, not the primitive. `TemporalReuse.solve` reuses last frame, reprojects it (backward_gather), +re-solves ONLY the dirty region, optionally accumulates (running average for noisy estimators). Tests: dirty-only +re-solve costs |dirty| calls not n AND matches a full re-solve exactly (clean cells verbatim); reproject is +hole-free; accumulation converges a noisy estimator; deterministic. Faculty `temporal_reuse`. The render/solve +SPEED discipline (path tracer / WoS / fluid). HONEST: reuse is correct only where the caller's dirty mask is +right -- a wrong mask reuses stale values (the owned failure mode). + +Five faculties wired default-off & delegating: faculties, spatial_index, reaction_diffusion, emergent_concepts, +temporal_reuse. The sweep's MEDIUM items (compose, pack/fountain/uri storage spine, directional SH audio+light, +lookahead->conditional Propagator) and LOCAL completions (materialio textures, sculpt fast-rep, near-surface SDF, +occlusion-speed) remain at their honest weight for a later pass. + +## Above/Below Sweep 3 -- the medium unifications (ranks 6-9) (+11 tests) + +The three "quiet unifications" the sweep said were the real prize -- places where separate backlog items are the +SAME mechanism and should share code. Probe-first: item 6 was already done, three were genuine builds that REUSE +existing modules (the unification only counts if the code is one). + +ITEM 6 -- `compose` (forward composition): ALREADY WIRED (compose_scene / compose_nested / realize present). No +new work; noted so a future pass doesn't re-propose it. + +ITEM 8 -- DIRECTIONAL SH, one primitive for SOUND *and* LIGHT (`holographic_spharm.py`, NEW). The sweep's catch: +real spherical harmonics were already in the tree for light (`prt.sh_eval`, l=0..3), and directional sound uses +the SAME basis (first-order ambisonics IS the l=0,1 SH of a sound field). So `sh_project`/`sh_reconstruct` are +built ONCE on prt's basis (no fork) and serve both: a directional LIGHT lobe reconstructs at err 0.03, a +directional SOUND gain at err 0.11, from the identical code path; higher order sharpens; scalar+RGB both work. +Faculties `directional_field` / `sample_directional`. HONEST: band-limited to prt's order (sharp features past +l=3 smooth; raise order at a coefficient cost). + +ITEM 9 -- CONDITIONAL PROPAGATOR (`holographic_condprop.py`, NEW). "Predict = bind a transform to a state" +unifies dynamics.Propagator, lookahead (per-action model), video (motion-as-bind), and backwardwarp (unbind +reproject). So lookahead's per-action forward model just IS a Propagator, conditioned on the action -- a dict of +`dynamics.Propagator`s, one per action, REUSING the dynamics module. Added `Propagator.learn_pairs` (additive; +learns the transfer from (state->next) PAIRS, the action-conditioned case). Fixes lookahead's kept negative: a +single averaged delta per action was too coarse; a full per-frequency transfer per action is the rich form, with +a regularised INVERSE for free (content-addressable planning). Measured on a state-graph (places=codebook atoms, +actions=permutations): every place transition lands EXACTLY (reanchor index correct); re-anchored planning +composes 7 hops exactly (anchored cos 1.00 vs naive 0.10 -- the cleanup-every-hop RAY-1 lesson); the inverse +recovers the prior place. Faculty `conditional_propagator`. HONEST: linear-in-Fourier per action (chaotic action +dynamics need the reservoir lift, dynamics' own boundary); raw per-action cosine is modest (~0.33 fitting 8 +permutation mappings with one operator) but the cleanup-gated argmax is exact -- planning rides on the reanchor. + +ITEM 7 -- STORAGE SPINE (`holographic_storage.py`, NEW). pack (dedup) + fountain (erasure) + uri (addressing) +are one LAYER: how a record-set is stored, deduped, made robust. `StorageSpine.put(tags, bytes)` keys by uri, +DEDUPS identical content by sha256 (stored once, many keys point to it), and codes it with a fountain; `.get(key, +loss=...)` recovers even under droplet loss, returning an honest None past the code's limit (never wrong bytes). +Measured: 3 payloads across 4 keys (dedup), recovery at 30% droplet loss. Faculty `storage_spine`. REUSES uri + +fountain (no reimplementation). HONEST: exact-content dedup only (near-dup delta packing stays pack's job); LT +code needs a small droplet overhead. + +Five faculties wired default-off & delegating: directional_field, sample_directional, conditional_propagator, +storage_spine (+ compose already present). REMAINING (honest): the sweep's LOCAL completions at their own weight +-- materialio texture maps, sculpt fast-rep, near-surface->full SDF, occlusion-recall speed (Batch-OMP) -- each +improves one thing (not a fanout mechanism), a smaller follow-up pass. + +## Above/Below Sweep 3 -- the local completions (backlog finished) (+5 tests) + +The last tier: the sweep's LOCAL completions (each improves one thing, not a fanout mechanism). Probe-first +shortened this the usual way -- TWO of the five were already built: + +ALREADY DONE (probed, not rebuilt): +* OCCLUSION-RECALL SPEED (Batch-OMP): `occlusion.build_gram(codebook)` + `occlusion_recall(..., gram=G)` (the + Rubinstein-Zibulevsky-Elad Gram-cached fast path, D out of the inner loop) AND `occlusion_recall_forest` + (sub-linear via HoloForest) are already implemented, wired (15 refs), and tested (22 tests). No work needed. +* SCULPT FAST REPRESENTATION: `holographic_sparsefield.py` IS FS-2 (the Adalsteinsson-Sethian / OpenVDB narrow + band) -- store/edit/re-extract only the thin shell around the surface, so a stroke costs O(brush). Built, + wired (4 refs), tested (8 tests). The "fast representation" the sculpt kept-negative pointed at already exists. + +BUILT / WIRED THIS PASS: +* MATERIALIO TEXTURE MAPS (`holographic_materialio.py`): the one genuine gap (the module flagged it "the natural + next step, not faked"). New `TextureMap` -- an (H,W,C) image sampled by UV with BILINEAR interpolation + (pixel-center convention; 'repeat'/'clamp' wrap). PBRMaterial now carries optional base_color/metallic/ + roughness/emissive maps and a `sample(u,v)` returning the effective glTF factor x texture values -- fully + backward compatible (no map -> the old factor-level behaviour, bit-identical). Faculty `texture_map`. Tested: + bilinear corner/centre exact, repeat-wrap, map x factor, backward-compat, deterministic. +* GRAPH_MEMORY -> NAV/HIERARCHY (fit-correct wiring, was 0 refs): faculty `graph_namespace` exposes GraphMemory + as a hierarchical namespace/navigation tree (observe_vector/classify_vector routes a query to its region). + KEPT NEGATIVE loud: this is for HIERARCHY/NAMESPACE/NAV, NOT exact recall -- graph_memory's recall accuracy + collapses at scale (a documented negative), which is exactly why the sweep re-homed it to navigation. Tested: + routes a query to the right region. +* NEAR-SURFACE -> FULL SDF (photo-to-3D, honest reuse): faculty `near_surface_to_sdf` thresholds a near-surface + signed band to inside/outside occupancy and runs the EXISTING fast-sweeping eikonal (signed_distance_field / + _3d, dispatched by dimension) to redistance everywhere. The extension mechanism already existed; this just + prepares its input from a band. Tested: a sphere band redistances to a full field, sign preserved. + +SWEEP 3 COMPLETE: the wide-reach five (bridge, spatial, automaton, emergence, temporal), the three medium +unifications (SH sound+light, conditional Propagator, storage spine), and the local completions (texture maps, +graph namespace, near-surface SDF) -- with occlusion-speed, sculpt fast-rep, compose, and diffusion found already +present by probe-first. The whole audit backlog is now cleared. + +## Render/Sim Pipeline -- usability: one configurable pipeline, primitives, field effects (+23 tests) + +The ask was blunt: "make sure the render/simulation pipeline is easy to use." Delivered the coherent usability +arc -- Phase 0 primitives (the foundation), Phases 1-2 the pipeline (the headline), Part 4 FieldEffect (a stage +that plugs in). Probe-first confirmed MT5 image-textures and G4 spatial index were already built (prior sessions). + +PHASE 0 -- PRIMITIVES PROMOTED (the shared building blocks): +* G1 sdf_normal: the surface normal is a property of the FIELD, not the renderer, so it now lives ONCE in + holographic_sdf.sdf_normal; holographic_raymarch.sdf_normal DELEGATES to it, verified BIT-IDENTICAL (same + central-difference evals). One normal for emission/collision/displacement/sculpt/field-effect falloff -- no + drift, no private copies. +* G2 holographic_integrate.py: the time-step in ONE place. semi_implicit_euler (symplectic -- velocity first, + then move: energy drift 0.0003 on an orbit vs 146 for explicit Euler, MEASURED), explicit_euler (the honest + baseline only), verlet (2nd-order, matches analytic free-fall). And SimStep -- the ONE interface (advance(dt, + ctx)) that every solver is WRAPPED behind (SolverAdapter), fixing the "fluid.step() vs softbody.step(dt,...) vs + physics.step(S_x,S_v)" signature mess. ParticleSim is a concrete SimStep on the shared integrator. + +PHASES 1-2 -- THE PIPELINE (holographic_pipeline.py, the usability win): +* PipelineConfig + presets (preview/final/interactive): the ONE opt-in surface, replacing scattered kwargs. +* build_pipeline(cfg): config -> ordered, validated Pipeline in three readable moves -- SELECT enabled stages, + AUTO-INCLUDE prerequisites (ask for SVGF -> the G-buffer stage is pulled in even though its own enabled() is + False, because svgf_denoise NEEDS "gbuffer" and it PRODUCES it), REJECT impossible combos at BUILD time with a + clear message (dirty_only without temporal_reuse; splat_proxy with quality=final; bad denoise value). +* Stable topological sort: order by data dependency (Kahn), tie-broken by (phase, name) so sim(0) < render(1) < + present(2) even where no data edge forces it, and the SAME config always yields the SAME order (deterministic). +* Pipeline.plan(): the DRY RUN -- lists every active stage + WHY, WITHOUT rendering (the "did the right + components get used?" answer). Pipeline.run(scene, seed, renderer=): threads a FrameState through the stages. +* Stage declares needs/produces and DELEGATES its work; manual assembly still works (config is sugar over a + stage list). KEPT LOUD: this is a stage-LIST + dep-dict + toposort, deliberately NOT a general DAG engine + (branching/loops are the machine's job -- the VSA-native endgame); the built-in demo renderer/sim are light + stand-ins so it runs end-to-end, a real app injects RenderSession / its solvers via the stage run closures. + +PART 4 -- FIELDEFFECT (holographic_fieldeffect.py, a stage that plugs in): +* A shaped zone of influence: the SDF is the shape, its signed distance IS the falloff coordinate (weight = 0 + outside/at surface, ->1 a radius deep in, edge shaped by sculpt.falloff), effect(points, weight) is what it + does. FieldGroup ADDS effects (force superposition is a commutative monoid -> order-independent). + AttachedFieldEffect rides a moving node (world points -> the node's frame via the inverse transform: "a rigid + transform is a single bind"). Ready effects attract_to/repel_from/uniform_force. KEPT LOUD: a soft weighted + force, NOT a hard constraint (sticky = strong short-range attractor, not a positional lock); mesh_to_sdf shape + inherits its sign caveat. + +FACULTIES: render_pipeline(preset, **overrides), field_effect(sdf, effect, ...), particle_sim(pos, vel, +force_fn, integrator). Integration: an interactive pipeline plans+runs a frame (sim before render, tonemapped +out) while a FieldEffect drives a ParticleSim inward and the G1 normal stays bit-exact. + +HONESTLY DEFERRED (own passes, NOT rushed -- rushing a bit-exact refactor is how you flip a downstream tie): +* Phase 3 materials/textures convergence (MT1 unify 4 material types on one Material, MT2 one BRDF from ~5 + copies, MT3 data layer, MT4 TextureSource over ~20 modules) -- a large risky bit-exact refactor. +* Phase 6 VSA-native endgame (lower PipelineConfig to a recipe, plan()->EXPLAIN, run on the machine via the + faculty->handler bridge, which already exists). +* Full Phase 0 delete-and-delegate of the OTHER normal/march copies beyond G1's (same bit-exact pin pattern). + +## Calibrated forecast confidence -- conformal keystone (F1 + F2 + F8) (+9 tests) + +The Forecasting backlog's ask: "forecast any data, use it IF we have a certain level of confidence." The engine +already PRODUCES the next vector four ways (Propagator/reservoir/predictive/generate); the missing piece was +CALIBRATED confidence on a forecast + a way to score it. Built the keystone cluster. Also: the project was +renamed to leCore -- the old README (research log with tour results + count markers) became researchLog.md, a new +user-facing README landed; modules keep the holographic_ prefix, canonical zip name unchanged. + +PROBE-FIRST: the backlog's own audit said "no conformal machinery anywhere" -- but holographic_reasoning.py +already had a SCALAR split-conformal ConformalPredictor (leOS's reflex gate). So F1 was NOT from scratch. Built +holographic_conformal.py to GENERALIZE that same finite-sample quantile (the (n+1)(1-alpha) order statistic -- +pinned identical, test_finite_sample_quantile_matches_reasoning_rule) with the four things it lacked: + +* F1 VSA-NATIVE + WRAP + ABSTAIN: nonconformity_vector = 1 - cosine(pred, actual) so a VECTOR forecast is scored + in the engine's own metric (the interval becomes a cosine-RADIUS = a prediction SET). ConformalForecaster + (scalar or vector) calibrates on held-out (pred, truth) pairs; wrap(producer, calib) makes ANY producer emit + (point, interval, abstain); abstain_width gates on trust. MEASURED: scalar coverage tracks nominal within 3 + pct across alphas; vector conformal covers 0.91 at the 90% target. +* F2 TEMPORAL (time series break exchangeability): AdaptiveConformal (Gibbs-Candes ACI) -- widen after a miss, + narrow after a hit -- holds LONG-RUN coverage under drift; weighted_conformal_quantile decays old residuals + toward recent. MEASURED: on a drifting stream ACI holds 0.90 coverage where FIXED split conformal drifts to + 0.41. Kept negative loud: ACI reacts after errors and only widens/narrows (can't fix a biased center); under a + ~0%-overlap regime change NO conformal variant holds -> abstain + flag drift (the honest earthquake boundary). +* F8 INSTRUMENT: coverage_report (the forecasting twin of calibration_report -- empirical vs nominal per alpha), + plus proper scores pinball_loss and crps_sample (coverage says the interval is WIDE ENOUGH; CRPS says the + forecast is GOOD -- rewards accuracy AND sharpness). MEASURED: CRPS ranks a sharp forecast (0.06) strictly + below a vague one (0.70). sharpness() reports mean half-width alongside coverage so neither is gamed alone. + +FACULTIES (general -> UnifiedMind per the backlog's §3): calibrate_forecast, adaptive_conformal, +forecast_coverage_report, forecast_crps. Integration: a producer's forecasts get a calibrated 90% interval that +abstains on tight tolerance, a drifting stream keeps ~0.90 via ACI, and CRPS ranks producers. + +KEPT NEGATIVES (loud): calibrated != correct (a useless predictor gets honest-but-WIDE intervals -- width is the +signal); coverage is MARGINAL not conditional (Barber 2021 -- holds on average over inputs, never for one +specific input); vanilla split CP is constant-width (CQR is the noted follow-on); the bit-exact/tie-sensitive +paths take no probabilistic wrapper. + +REMAINING F-SERIES (not built this pass -- own passes): F3 forecast(data) router; F4 forecasting-as-recall +(analog forecasting via HoloForest -- yields a distribution natively, pairs with F8's CRPS); F5 confidence-gated +generation; F6 multi-horizon trusted-horizon gate; F7 wire recurrent+market. And the §5 sweep (delegate the five +improvised confidences -- pathtrace variance, sbc validated, recall_calibrated, decide_confidence, raycoherence +-- to this one engine; the scheduler cost model IS a forecaster) is the high-leverage follow-on. + +## Backlog sweep: forecasting F3-F7, render-speed E (SVGF), query interface core (+20 tests) + +Three backlogs in one pass. Probe-first shortened all three -- the recurring lesson held. + +### Forecasting F3-F7 (completes the F-series, F1-F8 all in) +* F4 holographic_analog.py -- ANALOG forecasting ("find the past that looks like now, return what followed"): + pure HoloForest recall pointed at time, yields a DISTRIBUTION natively (pairs with F8 CRPS), ABSTAINS when no + analog. MEASURED: analog MAE 0.033 beats persistence 0.35 and mean 0.68 on a FAST quasi-periodic signal. + Kept negative loud: persistence is a strong baseline on SLOW signals (used a fast one so it's fair, not a + strawman); low-dim raw-window cosine abstention is unreliable (random vectors hit moderate max-cosine by + chance) -- the confidence ORDERING is robust, a hard floor needs high value or hypervector-encoded contexts. +* F6 holographic_horizon.py -- MULTI-HORIZON with a TRUSTED-HORIZON gate: per-step conformal quantile (reuses + holographic_conformal) that widens with horizon; trusted_horizon = leading steps within tolerance. MEASURED: + smooth system width 0.0016->0.036 trusted 15 steps; chaotic (logistic r=3.9) width blows to 0.77 -- Lyapunov + boundary made mechanical (predict-a-little-recompute-often). The "drive sim/render if confident" use case. +* F3 holographic_forecast.py -- the forecast(data) ROUTER: fit linear-AR AND analog on a train split, pick the + one with lower calibration MAE, wrap the winner in conformal. MEASURED: AR(1) routes 'linear' (0.079<0.091); + logistic map routes 'analog' (0.002<0.203 -- analog nails a deterministic map a linear window can't). Honest: + a linear window fits sums of sinusoids well, so quasi-periodic routes 'linear' -- used the logistic map for the + analog-wins case. Misroute fails SAFE (wide interval), never a confident wrong answer. +* F5 generate_gated -- confidence-gated generation: generate, score validity (cosine to nearest codebook atom), + accept/flag. Scoped: a filter/abstention aid for open-ended generation, not a correctness guarantee. +* F7 de-silo: recurrent (EchoStateNetwork/VSAReservoir) + market (RayProjector) were 0-ref; now reachable as + faculties recurrent_forecaster / market_projector. +FACULTIES: forecast, analog_forecaster, multi_horizon_forecaster, generate_gated, recurrent_forecaster, +market_projector. Integration: router->analog calibrated; analog abstains strict; horizon trusts a ramp fully. + +### Render-speed VSA-native cluster (technique E; A/C/D already existed) +PROBE-FIRST: robust/firefly accumulate (C) = holographic_accumulate.robust_accumulate ALREADY; SPRT adaptive +sampling (D) = holographic_honesty.SPRTRecall ALREADY; temporal reproject (A) = holographic_temporal.TemporalReuse +ALREADY; multires pyramid ALREADY. Only E was missing. +* E holographic_svgf.py -- edge-aware a-trous BILATERAL denoise: edge-stopping = product of RBF bumps on + normal/albedo/depth/colour (the bound-feature cosine, the ScalarEncoder's falloff), coarse-to-fine over the + pyramid. MEASURED against the plain-blur baseline: noisy 17.1 dB -> feature-aware 41.2 dB, beating edge-blind + blur 19.0 dB; edge MSE 0.0001 vs blur 0.0572. Kept negative: "a shared kernel is not a shared manifold" (the + edge-stop is measured, not assumed); it denoises, it can't add detail. Faculty svgf_denoise. + +### Query interface core (Phases 1-3; Phases 4-13 are follow-ons) +holographic_query.py -- a query IS a projection over the VSA store (a role-bound record IS a row). Phase 1 +projection core (from_rows binds categorical values to role vectors + bundles; project = unbind+cleanup). Phase 2 +a small documented SQL subset (SELECT/FROM/WHERE/ORDER BY/LIMIT, hand-rolled regex). Phase 3 the two things a +plain DB can't do natively: FUZZY WHERE (colour ~ 'grey' ranks by cosine) and per-row CONFIDENCE. MEASURED: +round-trip recovers gold/yellow; exact density>9000 ORDER BY LIMIT -> [gold, lead]; fuzzy 'colour ~ grey' ranks +{silver, iron, lead} at confidence 0.59. Kept negative loud: EXACT vs FUZZY is a real fork -- exact predicates +run on the STORED props (a decoded float has readback error), fuzzy on the VECTORS; the two modes are labelled, +never silently mixed (Table keeps both). Sparse schema = natural NULL (absent bind). Faculties: make_table, query. +DEFERRED (own passes): GraphQL resolver, aggregation/GROUP BY, the capability registry + EXECUTE (a "program" is +a queryable hypervector -- ties to holographic_machine), namespaces + user databases, saved views, persistence. + +## Render pipeline: wire the stand-in stages to REAL measured implementations + Phase 6 inspection (+2 tests) + +The render/sim pipeline shipped with light stand-in stages so it ran end-to-end; the deferred item was to wire +the real engine pieces in now that they exist (and SVGF was built this session). Done, backward-compatible, each +stage measured. PROBE-FIRST confirmed the sibling pieces were already in the tree (robust_accumulate, SPRTRecall, +TemporalReuse); only the wiring + SVGF were the work. + +* _render_run: now renders a two-surface demo scene with 8 noisy sample PASSES and ACCUMULATES them through + holographic_accumulate.robust_accumulate (firefly clamp). IMPORTANT API-GRANULARITY LESSON (kept): the winsorize + measures each WHOLE sample's deviation (norm of the full flattened array), so it rejects a corrupted sample PASS, + NOT per-pixel fireflies -- using it at the wrong granularity was a silent no-op (robust==naive). Used correctly + (one bad pass among 8): RMSE 0.06 vs naive 0.38. Per-pixel firefly clamp would need a per-pixel winsorize (noted). +* _gbuffer_run: real per-pixel (normal, albedo, depth) for the demo scene (shared _demo_scene helper so gbuffer and + render agree on the edge), replacing the random 8x8x4 stand-in. +* _svgf_run: calls the real holographic_svgf.atrous_bilateral using the gbuffer, MEASURED PSNR noisy->denoised + (~24.5 -> 28.3 dB on the already-accumulated frame). Falls back to the legacy blur if no real features (back-compat). +* _adaptive_run: self-contained (it runs independent of render -- both only need 'scene', so it cannot read + render's variance; ORDERING LESSON kept). Makes its own 3-sample variance probe + edge signal -> a real bool + "sample more" mask, and demonstrates the Wald SPRT stop (SPRTRecall needs null+match score distributions -- my + first call omitted them and was swallowed by try/except, a silent no-op; fixed to fit both distributions). +* _reproject_run: routes through TemporalReuse for the reuse bookkeeping (identity reproject -- the demo has no + camera motion; a real app warps by the camera delta, one bind, and re-solves only dirty cells). +* Pipeline.plan() is now a full EXPLAIN (stage, why, NEEDS, PRODUCES) -- Phase 6's inspection half, the pipeline + twin of the query interface's EXPLAIN. Backward-compatible (added keys). +Tests: test_wired_stages_are_real_and_measured (robust<0.5*naive, svgf denoised>noisy, real bool mask + SPRT +decision) + test_pipeline_deterministic. 10 -> 12 pipeline tests. + +HONESTLY STILL DEFERRED (own careful passes -- rushing a bit-exact refactor flips a downstream tie): +* Phase 3 materials/BRDF/texture convergence (MT1-4, ~20 modules, bit-exact) -- large risky refactor. +* Phase 6 EXECUTION half: run the lowered pipeline ON the machine VM. The blocker is real, not laziness -- the VM's + APPLY is acc->acc over a hypervector, but these stages transform a FrameState (buffers, images), so it needs the + frame represented as a hypervector accumulator first. The inspection half (plan->EXPLAIN) is done. + +STANDING HIGH-LEVERAGE FOLLOW-ON (not this pass): the forecasting §5 sweep -- delegate the five improvised +confidences (pathtrace variance, sbc validated, recall_calibrated, decide_confidence, raycoherence) to the one +conformal engine, and build the scheduler cost model (which IS a forecaster) on it. + +## Forecasting sec.5 sweep: delegate the improvised confidences to one calibrated engine (+9 tests) + +The sweep's reframe: conformal/calibrated confidence is a UNIVERSAL "estimate + confidence + abstain" wrapper, +and the codebase improvises it in ~5 places. PROBE-FIRST found THREE of the five already calibrated -- so the +job got shorter, the usual way: +* Resonator soft confidence (Olshausen x Cranmer, the panel's flagged "real remainder"): ALREADY BUILT. + decompose_structure(confidence=True) returns {agreement, pvalue} -- a null-calibrated soft confidence for + APPROXIMATE inputs (where the exact `validated` boolean is uselessly False), via _resonator_noise_null. Wired + through factor_composite. Not rebuilt. +* recall_calibrated (RecallNull) and decide_confidence (agent): already calibrated (recognition/agent side). + +Two sites were genuinely missing and built: +* SCHEDULER COST MODEL as a forecaster (sec.5.5, the highest-leverage item). holographic_superschedule packed to + the THEORETICAL wall (pack_capacity = 0.10*D, assumed). Added calibrated_capacity(dim, gated, target_recall): + probe growing superposition loads, measure GATED cleanup recall (recovered vector cleans up to its own atom), + return the largest load whose recall stays >= target -- a MEASURED capacity, the cost model as a forecaster + ("will this packing recall well?"). should_superpose(n, ...) gates a batch on the measured wall. MEASURED + (honest, useful): at D=512 the measured capacity is 34 @ target 0.90 and 24 @ 0.99, BELOW the theoretical dial + of 51 -- assuming 0.10*D OVERPACKS at a strict recall target. Kept negative: costs a probe; target/data- + dependent; assumes monotone crosstalk in load (true for random superposition). Faculty scheduler_capacity. +* RENDERER ADAPTIVE STOP (sec.5, renderer delegation). holographic_adaptive_sample.py: given a renderer's + per-pixel variance-of-the-mean, converged_mask (CI half-width within tolerance -> stop) and sample_budget + (extra samples to reach a target interval; 0 where converged). IMPORTANT HONEST CORRECTION kept loud: a + per-pixel MC estimate is a SAMPLE MEAN, so its interval is GAUSSIAN/CLT (half-width = z*sqrt(var-of-mean)), NOT + conformal -- conformal needs a per-pixel calibration set of residuals a single pixel doesn't have. The sweep's + "conformal everywhere" framing is corrected here to the estimator that actually fits. MEASURED: halving the + target interval quadruples the samples (the sigma^2/n MC law). Faculty adaptive_sample_budget. + +Integration: scheduler measured-capacity <= theoretical and stricter-target-smaller; adaptive budget 0 for +converged / >0 for noisy; resonator soft confidence reachable (already built). + +STILL OPEN in the sweep (own passes): raycoherence's bespoke fallback -> calibrated; abstaining depth/photo-to-3D +(the honest boundary as a mechanical abstention); per-peel/per-splat/store-drift delegations. And the big +remaining arc: query interface Phases 4-13 (GraphQL, aggregation, capability registry + EXECUTE, namespaces, +user DBs, views, persistence). + +## Query interface Phases 5-7: aggregation, the capability registry, EXPLAIN (+7 tests) + +Continuing the query-interface arc (Phases 1-3 shipped last pass). Probe-first shortened Part 2 again. + +* PHASE 5 -- AGGREGATION & GROUP BY (holographic_query). Query.group_by + Query.aggregate; run() collapses the + filtered rows into one row per group. COUNT/SUM/AVG/MIN/MAX are EXACT because they run on the STORED props (not + decoded vectors -- exactly why Table keeps both), plus a per-group _centroid = bundle of the group's records + (the VSA aggregate = the group's prototype). parse_sql extended: SELECT items classify as aggregate FUNC(col) + vs plain column, and a GROUP BY clause. MEASURED: GROUP BY colour -> grey(count 3, avg 9900) + centroid; + global MIN/MAX/SUM exact. Kept negative: COUNT/centroid clean, numeric aggregates exact ON STORED PROPS only. + +* PHASE 6 -- CAPABILITY REGISTRY (Part 2, "query the mind"). capability_registry(mind) introspects every public + faculty into a VSA table {name, domain, doc}, so "what can this mind do?" is an ordinary Part-1 query: + SELECT name FROM actions WHERE domain='forecasting' returns the forecasting faculties; GROUP BY domain is a + capability CENSUS (measured on the live mind: 601 faculties -> general 386, geometry 61, render 47, physics 39, + agent 20, kernel 14). Introspection = a data query over the registry -- the unification the backlog pointed at. + Kept negative loud: domain is a keyword HEURISTIC over the method name, not a curated taxonomy; doc is the first + docstring line. Faculty capabilities(). + +* PHASE 7 -- EXPLAIN = a DRY RUN (Part 2). explain_program(machine, program_vec): run with NO handlers, so every + APPLY is a no-op and the heavy work is skipped, but the machine walks the whole program -- the trace names which + faculties it WOULD call and how many steps, without executing them. MEASURED: [APPLY denoise, APPLY recall] -> + faculties_called ['denoise','recall'], n_steps 2. The program-level twin of the pipeline's plan()->EXPLAIN. + Faculty explain(machine, program_vec). + +PHASE 8 (EXECUTE) PROBE: the mind ALREADY runs programs with its handler bridge -- run_procedure(name_or_program, +init_acc) via _machine() + _procedure_handlers(). So "execute a VSA program" exists; "with QUERIED arguments" is +a thin composition (a SELECT yields a vector -> bind into init_acc -> run_procedure), not a new engine. Noted, not +rebuilt. + +STILL OPEN (own passes): Phase 4 GraphQL resolver for the nested scene; Phases 9-13 -- namespaces + the read-only +`system` wall, CREATE DATABASE/TABLE/INSERT for user databases, cross-namespace INSERT...SELECT (bookmarks), views +as saved program vectors, persistence via core.save/load. These are the "own your own database" half (Part 3). + +## Query interface Phases 9-13: own your own database over a read-only system wall (+9 tests) + +Finished the "own your data" half of the query arc (Part 3), all on the projection core. + +* PHASE 9 -- NAMESPACES + the read-only SYSTEM WALL. Database holds namespaces, each {writable, tables, views}. + 'system' is writable=False -- where the mind publishes its OWN tables (scene, the capability registry, recall); + user namespaces are writable. The ONE rule that makes opening the door this wide safe: SELECT from ANY namespace, + write only your OWN. Enforced at a single chokepoint (_require_writable) -- reads from system.* work, every write + is refused with a clear message. +* PHASE 10 -- CREATE DATABASE/TABLE/INSERT. UserTable(Table) adds insert (encode one more row -> a record via the + SHARED _encode_row helper, so there is ONE encoding, not two; from_rows refactored to use it too). create_database + (a writable namespace), create_table, insert. +* PHASE 11 -- BOOKMARKS (cross-namespace INSERT ... SELECT). insert_select runs a SELECT on a source (often system) + and inserts into a user table. Two honest flavours kept: mode='snapshot' copies the VALUES (never dangles, goes + stale) vs mode='reference' stores the source id and resolve_reference reads LIVE (always current, resolve-or-null + when the row is gone). Measured: snapshot copies the gold objects; reference resolves live and dangles to None. +* PHASE 12 -- VIEWS. create_view stores a SELECT; run_view re-runs it against the CURRENT source (a LIVE view = + a saved query = a saved plan). A materialised snapshot is just a UserTable filled by insert_select. +* PHASE 13 -- PERSISTENCE BY REPLAY. to_state saves each user table's (columns, dim, seed, rows) + view specs; + from_state REBUILDS by re-inserting -- deterministic and byte-identical because the seed fixes every atom, and + no vocab vectors need serialising. Only the user's writable namespaces are saved (system is the mind's live + state). Measured: reloaded rows/view identical; reloaded records np.array_equal to the originals. +* catalog() = SHOW DATABASES/TABLES as plain rows. run_db_sql = a small SQL skin (CREATE DATABASE/TABLE, INSERT ... + VALUES, SELECT ... FROM ns.table) with the wall refusing writes to system.*; bookmarks/views stay on the clearer + object API. + +FACULTIES: database() (ships with system.actions = the capability registry, so 'your db over a shared system' is +real out of the box), db_query(sql, db). Integration: read the mind's own capabilities through the db, the wall +refuses a system write, bookmark forecasting faculties into a user table, reload by replay identically. + +KEPT NEGATIVES (loud): snapshot-vs-reference is a real trade (stale vs dangle); a live view costs a re-run, a +materialised one goes stale; sparse schema = natural NULL (absent bind); the SQL here is a documented SUBSET (the +object API is the full surface). The whole query interface (Phases 1-13) is now complete EXCEPT Phase 4 (the +GraphQL resolver for the nested scene), which stays its own pass. + +## Query interface Phase 4: GraphQL resolver for the nested scene -- the arc is now complete (+6 tests) + +The last piece of the query interface. SQL fits flat tables; a SCENE is a nested graph (an object has a name, a +material, a transform; the transform has a position, a kind), and GraphQL -- "ask for exactly the nested fields +you want" -- is the right shape for it. The VSA mapping is clean and is the whole point: a nested field is +bind(role, sub_record), so a nested selection `transform { kind }` == unbind(record, transform) -> the sub-record +-> unbind(sub, kind) -> cleanup. "Ask for the nested field" == "unbind exactly that chain of roles." + +holographic_graphql.py: Scene encodes each object as a NESTED VSA record (categorical fields bind(role, atom), +nested dicts bind(role, sub_record) recursively); a small readable GraphQL parser (selection set with () args and +{} children, recursive descent); resolve() filters objects by a `where` arg and projects EXACTLY the requested +(possibly nested) fields per object. project_via_unbind demonstrates the VSA claim: a categorical leaf is +recovered by unbinding the role chain. MEASURED: '{ objects(where:{material:"gold"}) { name transform { position +} } }' returns [ring, coin] with ONLY name+transform.position (not id, not kind); transform->kind unbinds to +'rigid'. + +Kept negative (the honest exact/fuzzy fork, same as SQL): NUMERIC/LIST fields (a position [x,y,z]) are read from +the stored object, not decoded -- a float has readback error, so we keep it exact rather than pretend to decode +it. The GraphQL here is a documented SUBSET (selection sets, one where arg), not the full spec. + +FACULTIES: make_scene(objects), query_scene(graphql, scene). Integration: a gold-filtered nested query returns +exactly the requested shape through the mind; the nested field resolves via the unbind chain. + +THE WHOLE QUERY INTERFACE (Phases 1-13) IS NOW COMPLETE: projection core + fuzzy WHERE + confidence (1-3), +aggregation/GROUP BY (5), GraphQL for the scene (4), the capability registry + EXPLAIN (6-7) with EXECUTE already +present (run_procedure, 8), and user databases over a read-only system wall with bookmarks, views, and persistence +(9-13). One projection core, reached by SQL, GraphQL, introspection, and an owned database. + +## Forecasting sweep: abstaining photo-to-3D -- observe the front, abstain on the rest (+5 tests) + +The sweep's depth/photo-to-3D delegation, and the photo-to-3D backlog's honest core. Lifting a single photo to 3D +is an ESTIMATE, so it should carry a confidence and abstain where it does not know. Built holographic_photo3d.py: +* unproject(depth, fx,fy,cx,cy): the standard pinhole depth->3D-points lift, (H,W,3). +* depth_confidence: a geometric support score in [0,1] = valid (finite, >0) AND continuous (small relative depth + step -- NOT an occlusion edge) AND front-facing (|n_z| of the estimated normal -- not grazing). Abstain where + any support fails. +* photo_to_gaussians: unproject the CONFIDENT pixels into per-pixel 3D Gaussians (Splatter-Image/Flash3D shape: + one splat per pixel -- position, colour, radius from local spacing, confidence weight), abstain on the rest. +MEASURED: a two-plane depth (near z=1 | far z=3, meeting at an occlusion edge, plus one invalid hole) unprojects +to two flat front planes at z=1 and z=3; abstention fires exactly at the edge (avoiding the stretched sheet of +fake geometry naive unprojection makes) and on the hole; ~90% coverage, the rest abstained. + +KEPT NEGATIVES (loud): the deepest one -- a single view NEVER observes the BACK of an object, so this emits the +visible front surface and abstains on the back rather than guessing a watertight mesh ("we don't know the back" +made mechanical). And the honest-tool correction (same as the renderer stop): confidence here is a GEOMETRIC +SUPPORT score, not a calibrated probability -- one depth map gives no per-pixel calibration set, so support + +abstention is the estimator that fits, not conformal. FIXED a real bug found while writing: the radius used +np.gradient(points, axis=2-1) which is axis=1 (column spacing) TWICE instead of column + row -> corrected to +axis=1 and axis=0. + +FACULTIES: photo_to_3d(depth, colour, fx,fy,cx,cy), unproject_depth. Integration: a near/far edge lifts to front +surfaces at both depths and abstains at the edge, coverage high but < 1. + +REMAINING sweep tail: raycoherence already HAS a variance-fallback (var_tol) -- a reasonable bespoke uncertainty +test, not a glaring gap. The big deferred render items stand: Phase 3 materials/BRDF convergence (~20 modules, +bit-exact) and Phase 6 run-the-pipeline-on-the-VM (needs the frame as a hypervector). + +## Render pipeline Phase 6 complete: RUN the pipeline on the VM (+3 tests) + +The deferred Phase 6 execution half -- and the blocker I had flagged ("needs the frame as a hypervector") turned +out NOT to be real, which is why probing the VM before assuming beat guessing. The machine's APPLY does exactly +`acc = handlers[fac](acc)` -- it only does vector math on the PROGRAM (the instruction decode), NEVER on the +accumulator. So the FrameState can ride as the accumulator and the VM sequences the stages; no frame-as-vector +needed. + +* Pipeline.lower_to_program(machine): assemble APPLY(stage) for each stage in order + HALT -> ONE program vector + (the config-to-recipe step). The stage names are the program's faculty operands. +* Pipeline.run_on_vm(machine=None, ...): build a HoloMachine that knows the stage names, lower the pipeline, + hand it {stage_name: stage.run} handlers, and machine.run(program, init_acc=FrameState, handlers=...). The + same holographic machine that runs every other program now DRIVES the render pipeline. Returns (FrameState, + applied_stage_names). +MEASURED: run_on_vm produces a frame BIT-IDENTICAL to the direct for-loop run() (np.array_equal on the image); +the VM APPLY-ed all 7 preview stages in order; SVGF PSNR 28.3 either way. explain_program over the lowered +program names the stage sequence -- so plan()->EXPLAIN (inspection, done earlier) + lower_to_program + run_on_vm +(execution, now) close Phase 6 end to end. + +Kept honest: run_on_vm is not faster than the loop (it is the same stages plus a decode per instruction) -- the +value is UNIFICATION (the pipeline is a program like any other, inspectable/EXPLAIN-able/composable), not speed. +The APPLY decode is a cleanup-gated nearest-atom read, reliable here because ~9 stage atoms are near-orthogonal +in 1024-d. + +Tests: run_on_vm bit-identical to run; lower_to_program is one vector whose EXPLAIN names the stages. Integration: +the pipeline built from the render_pipeline faculty runs on the VM with the same result. + +RENDER PIPELINE STATUS: Phase 6 now complete (inspection + execution). The ONLY remaining deferred render item is +Phase 3 -- materials/BRDF/texture convergence (MT1-4, ~20 modules, bit-exact) -- a large risky refactor that +stays its own careful pass (rushing a bit-exact refactor across 20 modules is how a downstream tie flips). + +## Render Phase 3 (materials/BRDF convergence): probe-first audit + pinning harness + one real bit-exact merge (+10 tests) + +The last deferred render item. I did NOT rush the imagined 20-module refactor -- I probed first, and the probe +changed the plan (the usual outcome): + +AUDIT (kept honest, and mostly a set of NON-findings): +* BRDF is ALREADY canonical in holographic_brdf.py; raymarch DELEGATES to it (imports cook_torrance/fresnel), not + a copy. The other "shading refs" a grep flagged were FALSE POSITIVES -- a 'specular' word in a rayindex comment, + and FFT variables F0/F1 in dynamics (Fourier coefficients, not Fresnel F0). No rival inline BRDF to merge. +* The 3 material classes serve DIFFERENT layers -- Material (core PBR sockets), PBRMaterial (asset I/O), + SurfaceMaterial (render: a Lambert+Blinn model, with adapters FROM the PBR side). SurfaceMaterial uses a + DIFFERENT shading model on purpose, so folding it into the GGX Cook-Torrance BRDF would CHANGE behaviour -- a + behaviour change, not a bit-exact refactor, and forcing it would be WRONG. Adapters (from_matlib/from_name) + already converge the types at the boundaries. So "unify the 4 material types" is largely not-a-refactor. +So the honest outcome: the big risky refactor the backlog imagined mostly does not exist / should not be forced. + +DELIVERED INSTEAD (the disciplined groundwork + the one genuine merge): +* BIT-EXACT PINNING HARNESS (test_material_pinning.py): locks the canonical BRDF's exact outputs (fresnel_schlick + scalar 0.07 + vector metal branch; fresnel_dielectric; cook_torrance dielectric AND metal, pinned to their full + float tuples) and the delegation STRUCTURE (raymarch imports the canonical BRDF and defines no rival; exactly + one def of each canonical function; SurfaceMaterial does NOT import cook_torrance -- the different-model audit, + mechanical). If a copy ever drifts back or a value changes, a pin fails loudly. +* ONE GENUINE bit-exact merge: the metallic->F0 formula 0.04*(1-metallic)+base*metallic was inlined in THREE + places (cook_torrance, the sampler variant, and an inline COPY in raymarch:222). Extracted to one named helper + brdf.metallic_f0(base_color, metallic) -- readable (names the 0.04 dielectric constant, one home) -- and pointed + all three sites at it. VERIFIED BIT-EXACT: the cook_torrance value pins (0.20796842402070315..., 0.0627...) are + UNCHANGED, so every downstream shade is byte-identical; 66 shading/render tests green. metallic_f0 pinned: + metallic=0 -> 0.04 dielectric, =1 -> base metal, 0.6 -> [0.496,0.136,0.136]. + +This is the bit-exact pinning discipline the project uses everywhere (the G1 SDF-normal pin, the reasoning-quantile +pin) applied to shading. No tour block -- a bit-exact refactor is invisible to capabilities by design; the tour +still runs green because the render path is byte-identical. + +RENDER PIPELINE: with Phase 6 done last pass and Phase 3 resolved here (audit + harness + the one real merge), +there is no remaining risky materials refactor pending -- what the backlog imagined was mostly already done or not +safe to force, and what WAS a real copy is now merged and pinned. + +## Physics backlog Part 3 #1: the SpectralField backbone -- advance is one bind, any t closed form (+10 tests) + +The keystone of the physics/FX backlog (Thesis A made concrete). A linear field IS a hypervector and advancing it +is ONE bind -- a circular convolution, diagonal in the Fourier basis -- so we diagonalise once (the FFT) and any +time t is a closed-form multiply by a per-frequency transfer. Every linear domain is then a DISPERSION RELATION +on one backbone. holographic_spectralfield.py: + +* wavenumbers(shape, dx): the angular-|k| grids (N-D) via 2*pi*fftfreq. +* SpectralField(field, velocity, order, rate|omega, dx). TWO regimes because the physics has two shapes: + - PARABOLIC (diffusion/heat/gas): u_t=-D|k|^2 u -> u_hat(t)=u_hat(0)*exp(rate*t). One complex multiply/freq. + - HYPERBOLIC (wave/EM/ocean): u_tt=-omega^2 u -> state (field,velocity), exact per-mode rotation + u(t)=u0 cos(wt)+v0 sin(wt)/w (sin(wt)/w via t*sinc(wt/pi), valid at w=0). Still one bind, still any-t. + advanced(t) closed-form jump; step(dt,steps); add_source=BUNDLE (sources add, no re-sim); steady_state = the + t->inf mean; trigger_mask(potential,threshold) = the calibrated emission/breaking-onset trigger. +* poisson_solve: electrostatics as the CLOSED-FORM LIMIT -- phi_hat=source_hat/(eps0|k|^2), one spectral step. +* factories, one dispersion each: diffusion_field (-D|k|^2), wave_field / em_field (c|k|), ocean_field + (sqrt(g|k|), Tessendorf dispersion) + phillips_spectrum (seeded, Hermitian-real sea state). + +MEASURED (the standing rule -- BEAT the grid baseline, keep the negative if it loses): +* diffuse a Gaussian to t=20: SPECTRAL matches the analytic heat kernel to 1.1e-16 (machine precision) in ONE + eval; the grid diffuse_heat baseline (400 steps of dt=0.05) matches analytic to 1.1e-3. Spectral is exact and + cheaper -- a clean win. +* closed-form advanced(t) == stepped step(dt)*N to <1e-8 for BOTH regimes (the 'any t in closed form' property). +* a wave mode returns to itself after exactly T=2pi/(c|k|); superposition additive; ocean real+deterministic+ + dispersive; Poisson extremum at the charge. + +KEPT NEGATIVES (loud): ONLY LINEAR operators diagonalise -- nonlinear (overturning wave, shock) stays a grid +solver (Part 4 top rung, later). The spectral field assumes PERIODIC boundaries (the FFT world wraps); a hard +wall needs the grid path or a reflection trick. Hyperbolic needs a (field, velocity) pair, not just a field. + +FACULTIES (default-off): spectral_field, spectral_diffusion, spectral_wave, spectral_ocean, +electrostatic_potential. Integration: one backbone serves diffusion+wave+ocean+electrostatics through the mind. + +NEXT (Part 6 sequencing): wire remaining HAVE/PROMOTE dispersions; the FFT-ocean showpiece (ocean_field+phillips +are already here -- add the tour showpiece + foam/spray triggers); wave-packet field (N8, a bundle of role-bound +packets); the AdaptiveSolver (plan_waves, mirrors plan_render) -- the efficiency keystone; EM module; ice growth +(= lightning branching); rung-4 grid solvers (barrel, snow-MPM) LAST and honestly. + +## Physics backlog N8/#4: the wave-packet field -- tricky waves as a bundle of role-bound packets (+9 tests) + +The plain FFT ocean is GLOBAL (every wave spans the domain), so it cannot reflect off a wall or bend around a +rock. The wave-packet method (Jeschke & Wojtan) LOCALIZES the spectrum: the surface is many little Gaussian- +enveloped wave trains, each living at a PLACE, so each can reflect / shoal / diffract. holographic_wavepacket.py: + +* WavePacketField(size, g, envelope, seed): packets as readable parallel arrays (pos, k, amp, phase). + - _omega(|k|, depth): deep water sqrt(g|k|), finite depth *tanh(|k|h) (shoaling). + - _group_speed = d omega/d|k| by central difference -- the ENERGY speed (half the phase speed for deep water), + which is what we advect (a packet's energy, not its crests). + - advance(dt, depth, obstacles): phase += omega*dt; pos += group_velocity along k-hat; SPECULAR reflection off + the domain walls and axis-aligned obstacle boxes (k' = k - 2(k.n)n). + - render(res): surface = sum of amp * gaussian_envelope * cos(k.(x-pos)+phase) -- the localized-spectrum height. +* THE VSA FORM (N8): a packet IS a role-bound record -- bind(POS_X..PHASE, encode(value)) bundled; the surface IS + a bundle of those records, a content-addressable index (member cosine > stranger). packet_record / surface_bundle. + +MEASURED: a packet aimed at the far wall reflects (k mirrors, heads back); it moves at the group speed = exactly +half the phase speed (deep water, to 1e-3); shallow water slows it (shoaling, cg_shallow < cg_deep); an obstacle +box keeps packets out; the rendered surface is localized near the packet; the surface bundle is content- +addressable (a member outscores a stranger); deterministic (seeded). + +KEPT NEGATIVES / honest scope (loud): the PHYSICS runs on plain NumPy arrays -- clearer and faster than +unbinding/rebinding a record every step (the gratuitous-VSA-round-trip warning), so the record/bundle layer is +the REPRESENTATION (content-addressable index), not the simulation loop. Reflection is specular off axis-aligned +walls/boxes; a curved obstacle or full diffraction is an extension. This is the localized-spectrum RUNG; it does +not overturn (that is the grid solver, Part-4 top rung). + +FACULTY (default-off): wave_packets. Integration: a packet reflects off a wall through the mind + the surface is +a content-addressable bundle. + +Physics sequencing now: backbone (done) + wave packets (done, this) unblock the AdaptiveSolver (plan_waves, +mirrors plan_render) -- the next item, the efficiency keystone that dispatches fft_ocean / wave_packets / +shallow / free_surface per tile. Then EM module, ice growth (= lightning branching), rung-4 grid solvers last. + +## Physics backlog #5: the AdaptiveSolver (plan_waves) -- the ocean stack dispatched like the renderer (+7 tests) + +The efficiency keystone the backlog calls "what unlocks the full ocean stack, correctly." It mirrors plan_render +EXACTLY: named break-evens at the top, a pure DECISION LAYER that returns a plan with a reason per choice, then a +separate executor. holographic_waveadaptive.py: + +* plan_waves(height, depth, obstacles, tile): per tile, pick the wave method from the LOCAL regime and say WHY -- + free_surface (breaking: local steepness > threshold), shallow_water (depth < threshold: shoaling/run-up), + wave_packets (obstacle within radius: reflection/diffraction), else fft_ocean (open water, cheapest). No + solving; inspectable before running (like plan_render/EXPLAIN). Tie-break FIXED & documented (breaking > + shallow > obstacle > open) so the plan is DETERMINISTIC -- the backlog's determinism rule. +* plan_cost / method_counts / all_one_method_cost: the efficiency measurement (relative method cost weights). +* solve_waves(plan, field, dt, methods, halo): execute -- run each tile's stepper on the tile+halo and OVERLAP-ADD + with a Hann partition-of-unity weight, so tile borders have NO seam. Steppers are pluggable. + +MEASURED: on an open sea with one breaking crest + a shallow shore strip + a rock, plan_waves assigns fft_ocean to +the majority of tiles; the adaptive plan costs 181 vs 3200 to run the grid solver everywhere -- 94% CHEAPER. The +tie-break is deterministic and breaking wins over shallow. solve_waves dispatches per tile (verified with tagging +steppers) and blends a smooth field with a border jump < 0.2 (no hard seam). + +KEPT NEGATIVES (loud): the win is REAL BUT BOUNDED -- a sea breaking EVERYWHERE gets NO discount (measured: +plan_cost == all-free_surface cost); adaptive dispatch makes the dear rung LOCAL, not free. And the free_surface +(overturning-barrel GRID solver) is the deferred rung-4 item: plan_waves correctly IDENTIFIES where it's needed +and runs the cheap methods everywhere else, but the actual breaking solver is NOT built -- the default +free_surface stepper is an HONEST placeholder (steepness clamp), flagged loudly. The dispatch (the thing that +"unlocks the stack") is complete and measured now; the grid solver itself is item 8. + +FACULTIES (default-off): plan_waves, solve_waves. Integration: the plan dispatches per tile through the mind, is +far cheaper than all-grid, and solve_waves runs + blends it. + +Physics sequencing: backbone + wave packets + AdaptiveSolver now done (items 1,4,5). Remaining: EM module (#6, +a SpectralField with c|k| + Lorentz), ice growth (#7, = lightning's diffusion-limited branching), and the rung-4 +GRID solvers (#8: the overturning barrel free-surface + snow-MPM) -- LAST and honestly, invoked THROUGH this +AdaptiveSolver so they stay local. + +## Physics backlog #6: the EM module -- Maxwell FDTD + the Lorentz force (+8 tests) + +The spectral backbone already gave EM two thirds for free: em_field (wave propagation, omega=c|k|) and +poisson_solve (the Coulomb/electrostatic limit). What was missing -- and what makes it electro-MAGNETISM -- is +the E<->B COUPLING and the force on charges. holographic_em.py adds both, with the two classic readable schemes: + +* THE COUPLED MAXWELL SOLVER: Maxwell1D, a Yee-grid FDTD. Ez and Hy live on interleaved half-grids and update + each other (Faraday: dH from curl E; Ampere: dE from curl H). That mutual feedback IS electromagnetism: a pulse + propagates at c = 1/sqrt(mu*eps). default_dt = 0.5*dx/c (Courant 0.5); the CFL hard limit is dx/c. +* THE LORENTZ FORCE F = q(E + v x B) + the BORIS pusher (boris_push / push_particle). Boris splits the step into + half-E-kick / EXACT-B-rotation / half-E-kick, so the magnetic rotation preserves |v| -- the integrator conserves + energy in a magnetic field, unlike naive Euler which spirals. exb_drift = (E x B)/|B|^2. + +MEASURED against known physics: F=q(E+vxB) exact; a charge in uniform B traces a CYCLOTRON circle (radius +m v/(qB), omega_c=qB/m) with speed conserved to 1e-9 and returning after one period; crossed fields give the +E x B DRIFT (avg vx 0.95 ~ E/B, charge-independent); the FDTD pulse front moves at c (verified for c=0.5 too); +energy is BOUNDED below CFL and BLOWS UP (>100x) above it -- the classic FDTD stability rule, kept as a test. + +KEPT NEGATIVES (loud): the Maxwell FDTD is a genuine GRID solver -- the coupled first-order curl equations do NOT +diagonalise into one bind the way a single wave component does, so it lives BESIDE the spectral backbone, not on +it (the standing single-field-is-spectral / coupled-is-grid line). It is 1-D here (Ez, Hy); 2-D/3-D add the other +components but no new ideas. The leapfrog energy oscillates ~few% at a single instant (E and H are staggered in +TIME) -- so the honest test is STABILITY (bounded) + propagation-at-c + CFL, not machine-exact conservation. + +FACULTIES (default-off): lorentz_force, push_charge, maxwell_field. Integration: Lorentz force + a cyclotron +orbit + a coupled Maxwell pulse through the mind. + +Physics sequencing: backbone + wave packets + AdaptiveSolver + EM now done (items 1,4,5,6). Remaining: ice growth +(#7 = lightning's diffusion-limited branching, reuse grammar/flow/diffusion) and the rung-4 GRID solvers (#8: the +overturning barrel free-surface + snow-MPM) -- last and honestly, invoked through the AdaptiveSolver. + +## Physics backlog #7: diffusion-limited branching -- ice dendrites AND lightning from one engine (+7 tests) + +N11's insight made real: ice/frost dendrites and lightning bolts are the SAME physics -- a cluster growing into +the steepest gradient of a Laplace (diffusion) field, branching stochastically. The classic dielectric-breakdown +model (Niemeyer-Pietronero-Wiesmann 1984; = diffusion-limited aggregation, Witten-Sander 1981). +holographic_dendrite.py: + +* _relax_potential: solve Laplace's equation by Jacobi relaxation -- phi=0 on the cluster, phi=1 on the source + (the far boundary / ground it reaches toward), smooth average between. This IS the diffusion field's steady + state -- the same Laplace/Poisson the spectral backbone solves; for lightning phi is literally the electric + potential. Warm-started between growth steps. +* DielectricBreakdown(shape, eta, seed): seed_point (a snowflake) / seed_line (frost on a window, or a cloud) + + set_source_border (radial, or 'bottom' for a bolt pulled to ground). grow(steps): the empty cells touching the + cluster grow with probability proportional to phi^eta -- growth RACES toward the steepest field. eta is THE + knob: ~0 bushy (Eden blob), 1 fractal dendrite, large stringy (a bolt). fractal_dimension via box counting. +* ice_dendrite() and lightning() are the SAME engine -- only the seed and the source boundary differ (N11: build + once, get frost and bolts). + +MEASURED: phi obeys Laplace (0 on cluster, 1 on source, monotone between, steeper near the border); an ice +dendrite grows a connected SPARSE FRACTAL (251 cells, box-dimension ~1.10, far more cells than a single line but +nowhere near a filled disk); lightning (same code, seed the cloud + attract the ground) reaches depth 63; higher +eta reaches deeper per cell (stringier); deterministic given the seed. + +KEPT NEGATIVES (loud): this is a LATTICE model on a plain NumPy grid -- it earns NO holographic form (growth is a +discrete stochastic choice, not a bind; the memory's don't-over-holograph-a-grid rule). The fractal dimension is +STOCHASTIC and eta/size-dependent, and with a border source at eta=1 the growth FINGERS (dimension ~1.1, like a +real Lichtenberg figure) rather than the idealized isotropic-DLA ~1.7 -- so the test checks a fractal RANGE +(sparse branching, not a line or a disk), not a fixed number. Anisotropy (six-fold snowflake symmetry) and surface +tension are extensions. The Laplace solve is simple Jacobi (readable over fast). + +FACULTIES (default-off): grow_ice, grow_lightning, dielectric_breakdown. Integration: one engine makes both the +ice dendrite and the lightning bolt through the mind. + +Physics sequencing: items 1,4,5,6,7 done (backbone, wave packets, AdaptiveSolver, EM, ice/lightning). ONLY the +rung-4 GRID solvers remain (#8: the overturning-barrel free surface + snow-MPM) -- the research-heaviest, done +last and honestly, invoked THROUGH the AdaptiveSolver so they stay local. + +## Physics backlog #8 (rung 4, part A): the OVERTURNING free surface -- the barrel a height field can't hold (+7 tests) + +The top rung of the adaptive ladder, and the honest reason it exists: every method below it stores the water as a +HEIGHT FIELD h(x) -- one height per position, single-valued. A breaking wave's crest curls FORWARD over its own +base, so above one x there are two surfaces (the falling jet, the wave face). A height field literally cannot +express that; neither can a grid velocity field. Overturning needs PARTICLES. holographic_freesurface.py: + +* FreeSurface(g, ground, damping): surface particles (pos, vel) under gravity. advance() flies them ballistically + and rests them on the ground. is_overturning() = the surface FOLDED (a particle that started behind another + ended up ahead -- an order inversion). is_multivalued() = two sheets at one x. settle_height(bins) = collapse + the fold back to a single-valued height (the handoff back to the spectral field). +* seed_breaking_crest(fs, crest_speed, phase_speed, ...): a PLUNGING BREAKER -- the crest tip thrown forward + faster than the wave travels (crest_speed > phase_speed IS the breaking condition: orbital velocity beats phase + velocity), so it outruns the base and plunges. +* free_surface_step(region, dt): the AdaptiveSolver's REAL free_surface stepper -- seed particles from a steep + tile, fly one step, settle back to a height. This REPLACES the old steepness-clamp placeholder, closing the + loop between item #5 (the dispatch decides WHERE breaking happens) and item #8 (the grid rung that does it). + +MEASURED: a steep crest (orbital speed 8 > phase speed 3) OVERTURNS -- mid-plunge the surface folds into a +multi-valued sheet (9 order inversions, 14 particles airborne over the wave face) that no height field can hold; a +gentle crest (3.2 vs 3.0) stays single-valued; the jet settles back to a single-valued height for the handoff; +free_surface_step runs on a tile; deterministic. The AdaptiveSolver's 6 tests stay green with the real solver +wired in. + +KEPT NEGATIVES (loud, the VFX-vs-physics line): this is a BALLISTIC-particle model of the plunging crest -- the +standard visual-effects approach -- NOT incompressible Navier-Stokes. Once airborne the jet IS in free fall, so +ballistics is right for the throw and plunge; what it does NOT model is pressure/incompressibility and the +turbulent whitewater AFTER impact. It captures the OVERTURNING TOPOLOGY (the multi-valued surface, the whole +reason for this rung) and leaves post-impact mixing as the documented gap. A production FLIP/PIC/SPH/MPM with a +pressure projection is the research-heavy extension; the AdaptiveSolver localizes WHEN this runs, not how hard the +full version is to write. + +FACULTIES (default-off): free_surface, break_wave. Integration: the barrel overturns through the mind, a gentle +wave doesn't, and the AdaptiveSolver's free_surface method is the real solver now. + +Physics sequencing: items 1,4,5,6,7 and 8A done. ONLY snow-MPM (8B) remains -- the Material Point Method +(Stomakhin 2013), the last item: P2G -> grid update (gravity + elasto-plastic stress) -> G2P -> advect, with the +same VFX-vs-physics honesty. + +## Physics backlog #8B (rung 4, part B, THE LAST ITEM): snow via MLS-MPM -- P2G/G2P ARE bundle/readout (+7 tests) + +The Material Point Method (Stomakhin 2013; compact MLS transfer, Hu 2018). Snow is elasto-PLASTIC: elastic up to a +yield, then permanent (a snowball packs, a footprint stays). MPM carries a deformation gradient F per particle and +clamps its singular values when it yields. holographic_mpm.py, MPMSnow: seed_block -> step (P2G -> grid update -> +G2P -> advect + SVD plastic clamp). + +THINKING HOLOGRAPHICALLY (the ask, and it turned out to be REAL, not forced): MPM looks like a pure grid solver, +but its transfer IS the engine's bundle/readout in a physics costume -- +* P2G (scatter each particle onto the grid through a kernel) is a SUPERPOSITION: grid_node = SUM_particles + weight*value. That is a BUNDLE of kernel-weighted, position-bound contributions -- the SAME operation as + splat_render ("a splat scene IS a bundle") and the RBF encoder (a bundle of encoded points = a KDE, Bochner). + VERIFIED, not asserted: the P2G mass grid EQUALS an independent bundle of kernel splats to 1e-9. +* The quadratic B-spline weights are that kernel -- a partition of unity (sum to 1 = a normalized bundle), so the + bundle preserves total mass (verified). +* G2P (gather back) is the READOUT -- query the bundle at the particle position. The P2G->G2P round-trip conserves + total momentum (verified) -- bundle->readout fidelity, "as above so below". +* The node accumulation is a COMMUTATIVE MONOID (mass/momentum ADD) -- exactly what holographic_distribute + partitions and reassembles; P2G is RAID-style width. +The GRID UPDATE (per-node corotated stress + SVD plastic clamp) is genuinely NOT holographic -- nonlinear local +physics, plain NumPy, no bind (the don't-over-holograph-a-grid rule). So MPM is a HYBRID: a holographic transfer +around a grid-native constitutive update -- and that hybrid is the honest picture. + +MEASURED: P2G == bundle of splats (1e-9) and conserves mass; P2G->G2P conserves momentum; a snow block FALLS under +gravity (CoM 11.9 -> 1.3) then PILES and COMPRESSES plastically (top 16.0 -> 3.3, vertical extent 8.0 -> 2.3, no +rebound) with mass conserved; deterministic. + +KEPT NEGATIVES (loud, VFX-vs-physics): readable 2-D demonstration. Constant Lame params (Stomakhin's +hardening-by-compression exp(xi(1-Jp)) omitted for readability); explicit integration (CFL cap, no implicit +solve); PIC-flavoured transfer (dissipative -- APIC/FLIP reduce that); 2-D. Production snow (hardening, implicit, +3-D, sand/mud) is the research-heavy extension. + +FACULTIES (default-off): snow_mpm, simulate_snow. Integration: snow falls/compresses through the mind, and P2G is +verified to be a bundle. + +*** THE PHYSICS & FX BACKLOG IS COMPLETE *** -- items 1 (SpectralField backbone), 4 (wave packets), 5 +(AdaptiveSolver dispatch), 6 (EM: Maxwell FDTD + Lorentz), 7 (ice/lightning diffusion-limited branching), 8A (the +overturning free surface), 8B (snow-MPM). The through-line the whole backlog proved: LINEAR field physics IS VSA +algebra (advance = a bind, superposition = a bundle) and even the hard GRID rungs are holographic where it counts +(MPM's transfer is bundle/readout) and honestly grid-native where it isn't (the constitutive/coupled updates). + +## Generalize-on-contact: the shared SCATTER/GATHER primitive (the bundle/readout under every transfer) + pipeline update (+8 tests) + +Asked to apply the MPM insight (P2G is bundling, G2P is the readout) elsewhere. Probed the stack: the SAME +local-kernel scatter/gather is written out by hand in >=3 modules, each with its own kernel -- +* holographic_fields.scatter_to_field / sample_field (BILINEAR, cloth<->fluid coupling; its docstring already + called scatter_to_field "the ADJOINT of sample_field"), +* holographic_mpm P2G / G2P (quadratic B-SPLINE), +* holographic_splat / holographic_kde (the same bundle with a GLOBAL Gaussian kernel). +So it is the classic §5.1 case: extract ONCE, make the call sites thin. holographic_transfer.py: +* scatter(points, values, shape, kernel, periodic) = the BUNDLE: grid_node = SUM_points weight*value (a + superposition of kernel-weighted, position-bound contributions). Deposit == bundle. +* gather(field, points, kernel, periodic) = the READOUT: value = SUM_nodes weight*grid_node. Sample == readout. +* Kernels: bilinear (2-node) and B-spline (3-node); general-D via itertools.product over the stencil; scalar or + vector (N,C) values. +VERIFIED (not asserted): scatter/gather are ADJOINT (=) for both kernels; a +partition-of-unity kernel preserves the total (a normalized bundle -> mass/momentum conservation); and this ONE +primitive reproduces fields' bilinear scatter/gather to 1e-12 AND MPM's B-spline P2G to 1e-10 -- proof the fluid +coupling and the material-point transfer are the SAME bundle/readout operation. + +MADE THE CALL SITES THIN: holographic_fields.scatter_to_field / sample_field now DELEGATE to the shared primitive +(thin wrappers; the (x=col,y=row)->grid[y,x] convention handled by a coord swap). Regression: 70 fluid/cloth/ +softbody/MPM/transfer tests green -- no behavior change. MPM's P2G/G2P stay inline (their transfer is woven with +the affine/stress term) but are verified identical; splat/kde are the same bundle with a GLOBAL kernel (documented, +left in place -- a global kernel has a different cost structure than a 3-node stencil, the kept scope line). +FACULTIES (default-off): scatter_to_grid, gather_from_grid. + +RENDER PIPELINE + DEFAULTS updated to reflect the new physics/FX (holographic_pipeline.py): the per-frame FX sims +we built are now opt-in capabilities in the centralized PipelineConfig -- waves (the adaptive ocean/wave dispatch: +fft ocean / packets / shallow / breaking) and granular (the MPM snow/sand/mud solver), each a flag + a phase-0 +sim stage, plus an ocean() preset (preview + waves). Defaults are CONSERVATIVE (waves/granular default False -- +backward-compatible, no existing pipeline changes), matching fluid/softbody. RenderSession defaults +(256x256, spp 64, max_bounce 4) audited and left as-is (sensible). Pinned: defaults off + stages gate on their +flags + build_pipeline selects sim_waves under ocean(). + +KEPT NEGATIVES (loud): the shared primitive unifies the LOCAL compact-support transfers (bilinear, B-spline); the +Gaussian splat/KDE are the same bundle at the GLOBAL end of the support spectrum, recognized but left in their own +modules by design (different cost structure). EM and dendrite are NOT wired as pipeline sim stages (they are not +per-frame scene FX the way waves/granular are) -- available as faculties, honestly out of the render pipeline. + +## Modeling-app backlog item 0: the canonical Scene document + stable handles + change notification (A+B+E) (+9 tests) + +Starting the modeling-app support backlog at its keystone (the backlog's own sequencing: do this before the +feature layer or the features have nothing coherent to attach to). holographic_scene_doc.py, class Scene: + +* A. ONE mutable document owning the cross-cutting state -- objects (handle -> SceneObject record: name, transform, + geometry, material, tags, params), a hierarchy (handle -> parent map), cameras, lights, the current selection, + and the undo history. Every tool edits and every output reads this one document instead of the fragmented + RenderSession / scenegraph / anim / solver state. +* B. STABLE handles -- THE fix. Object ids elsewhere are CONTENT hashes (great for dedup) but they CHANGE every + edit, so they dangle as handles (a selection/material/anim target pointing at the object breaks the moment you + move a vertex). Scene mints a PERMANENT random hypervector atom as the identity at creation (content-independent, + survives every edit) and keeps the content hash SEPARATELY, used only for dedup. Verified: editing geometry + changes _content_key but NOT the handle or handle_vector, so the selection still resolves. +* E. Change NOTIFICATION -- on_change(callback) fired on every add/edit/remove/select/undo/redo, so a viewport or + property panel stays in sync for free. + +Because every mutation goes through add/edit/remove, UNDO is automatic: a thin snapshot-swap stack records a cheap +before/after copy of the ONE affected record (O(one record), not the whole scene) and undo/redo swap between them. +The snapshot copies the mutable fields (transform array + tags/params dicts) but keeps geometry/material by +reference (replaced wholesale on edit) -- so big meshes aren't deep-copied. Identity atoms are never touched by +restore, so a handle stays valid across undo/redo. + +MEASURED (selftest + 8 pytests + integration): add returns a stable handle and fires an event; an edit changes the +content hash but not the handle/identity/selection (the B guarantee); all mutations fire change events; undo/redo +restore state and preserve identity (edit reverts/re-applies, add-undo removes then redo re-adds with the same +handle, remove-undo restores); hierarchy parenting works; content hash is deterministic and geometry-sensitive; +identity atoms are deterministic by seed. FACULTY (default-off): new_scene. + +KEPT NEGATIVES (loud): the document-level undo snapshots the RECORD (transform + small dicts + geometry-by- +reference); a geometry-heavy in-place edit would want holographic_scenedelta's O(change) geometry delta and +holographic_history.VersionedStore for VSA-row versioning -- this snapshot stack composes with those but doesn't +replace them. The hierarchy is a plain parent map (world-transform flattening still goes through +scenegraph.flatten_scene). Selection is a plain set of handles here; a NAMED saved set of thousands should store an +id list, not one giant bundle (the backlog's capacity ceiling) -- that's the feature-layer item, not this one. + +Next in the modeling-app backlog (per its sequencing): promote the recipe as the modifier stack + dep graph (C) + +parameter introspection (D); then cancellation (F) + transform utilities (G) + the API facade (H); then the +feature layer (selection/search/tagging riding the query layer, undo/redo stack, measurement/units, overrides, +snapping, grouping/instancing, camera controller) and finally the Sampler. + +## Modeling-app backlog C+D: the modifier stack + dependency graph (O(change) re-eval) + parameter introspection (+9 tests) + +Promotion, not a build (the backlog's "biggest underused win"). recipe.StructureRecipe is already an ordered, +non-destructive op sequence with stable handles; recipeops gives validate/reorder/substitute; dirtyfield is the +O(change) idea. holographic_modifier.py carries that pattern to a modeling app's per-object modifier stack over +ANY payload (mesh/field/vector) and adds the one thing a DEPENDENCY GRAPH needs that a plain recipe doesn't: +O(change) re-evaluation. + +* ModifierStack(base): base + an ordered list of Modifier(handle, name, op, params, specs, muted). evaluate() + folds the ops over the base non-destructively (base never mutated). The op contract: op(payload, **params) -> + NEW payload (must not mutate its input) -- the non-destructive modifier convention. +* O(CHANGE) RE-EVAL (the dependency graph): a `_dirty_from` frontier. Any change (set_param / set_muted / insert / + remove / move) moves the frontier to that index; evaluate() recomputes ONLY from the frontier down, reusing the + cached results above. Measured: on a 4-modifier stack, changing the 3rd modifier's param re-runs only 2 ops (not + 4); changing the base modifier re-runs all 4. "Each modifier depends on the one before, recompute only + downstream" IS the dep graph -- dirtyfield's O(change) on a linear op chain. +* Stable handles across reorder/insert/remove/mute (a panel or animation targets one modifier); mute skips a + modifier without removing it (no op call); validate() (well-formedness + declared param min/max). +* Item D -- describe(handle) lists a modifier's params as a schema {name,type,value,default,min,max}; the standalone + describe_object(obj) does the same for a SceneObject's roles (name/material/tags/params). Introspection = list a + record's roles, the property panel's data. + +FACULTY (default-off): modifier_stack. Integration: a Scene object's geometry is produced by a stack; tweaking a +modifier param re-evaluates O(change) (only 1 op) and writes back through scene.edit (firing a change event) with +the object's stable handle unchanged -- C composed onto item 0. + +KEPT NEGATIVES (loud): the op contract requires PURE ops (return new, don't mutate) so the cache stays valid -- +stated, not enforced (a mutating op would corrupt the reuse; that's the standard modifier contract). The stack is +a LINEAR chain (each modifier consumes the previous result), which is the common modifier-stack case; a general +DAG of dependencies (one modifier feeding several) is the recipe's bind-tree, not this linear promotion. The base +is shared by reference into the first cached stage; a base-mutating caller would break non-destructiveness (same +pure-input contract). + +Modeling-app backlog progress: item 0 (canonical Scene + handles + events) and C+D (modifier stack + dep graph + +introspection) done. Next per sequencing: cancellation (F) + transform utilities (G) + API facade (H), then the +feature layer (selection/search/tagging on the query layer, undo/redo stack, measurement/units, overrides, +snapping, grouping/instancing, camera controller) and the Sampler. + +## Modeling-app backlog F+G+H: cancellation + transform utilities + the API facade (the responsiveness & front-door tier) (+16 tests) + +Three small, high-leverage pieces that make leCore something you can BUILD AN APP ON. + +**G -- transform utilities (holographic_transform.py).** The engine had scattered transform bits (scenegraph, +cosserat, splatexport) but not the full gizmo/property-panel kit in one place. Gathered the standard math, readable +and well-commented: decompose(M) -> (translate, rotation-quaternion, scale) and compose_trs (the inverse) -- what a +move/rotate/scale gizmo reads off a matrix; a quaternion kit (from/to matrix via Shepperd, from/to euler +(R=Rz@Ry@Rx), from/to axis-angle, multiply, SLERP with shortest-path + lerp-fallback, rotate-a-vector); look_at +(OpenGL view matrix, -z forward, matching the engine Camera). Conventions stated ONCE (column vectors, quat=(w,x,y,z) +unit, euler XYZ, OpenGL look_at). Verified: decompose<->compose round-trips a T/R/S matrix; quats round-trip through +matrix/euler/axis-angle; slerp hits endpoints, stays unit, halfway-in-angle at t=0.5, and takes the short arc even +when a sign is flipped; look_at sends eye->origin and target->-z. NOT holographic -- plain linear algebra, kept a +small utility (not dressed as a bind). + +**F -- cooperative cancellation (holographic_cancel.py).** Progress hooks existed but nothing could STOP a running +render/sim. CancelToken: a minimal boolean flag with a cooperative check (should_stop() / callable / bool), reset for +reuse; run_cancellable(iterable, token) wraps any step loop. Threaded should_stop into path_trace (checked BETWEEN +passes -> returns the partial image; costs nothing per sample) and RenderSession.render_final (passthrough). +Verified: a token cancels path_trace early with a valid partial image, and should_stop=None is BIT-IDENTICAL to +omitting it (backward-compatible; 10 pathtrace/session tests green). + +**H -- the API facade (lecore.py).** One curated front door so a builder needn't know which of ~280 holographic_* +modules to import: lecore.scene (Scene, SceneObject), lecore.model (ModifierStack, describe_object, SDF primitives, +key mesh verbs), lecore.render (RenderSession, path_trace, CancelToken, PipelineConfig), lecore.sim (plan_waves, +solve_waves, FreeSurface, MPMSnow, StableFluid, scatter/gather), lecore.transform (the whole G kit). Re-exports +EXISTING names only (no new engine code); each module's docstring stays authoritative. areas() maps the surface. + +FACULTIES (default-off): look_at, decompose_transform, cancel_token. Integration: decompose a Scene object's T*R*S +transform for a gizmo (recompose round-trips), a mind cancel_token stops cooperatively, look_at aims at the object +-- G+F riding item 0. + +KEPT NEGATIVES (loud): decompose assumes NO shear (the T*R*S modeling-app case); a sheared matrix would need a +polar/QR decomposition (noted, not handled). quat_to_euler pins roll to 0 at gimbal lock (pitch ~ +/-90) -- the +standard ambiguity, made explicit. Cancellation is COOPERATIVE (checked between chunks), not preemptive -- a single +uninterruptible chunk still runs to completion; the cadence is the chunk size. The facade is curation, not a +sandbox -- it re-exports the real objects, so misuse of an underlying API still reaches the real code. + +Modeling-app backlog progress: item 0 (Scene+handles+events), C+D (modifier stack+dep graph+introspection), and +F+G+H (cancellation+transforms+facade) done -- the whole FOUNDATION + responsiveness/front-door tiers. Next per +sequencing: the FEATURE layer -- selection/search/tagging on the query layer, the undo/redo stack, measurement/units, +render overrides, snapping, grouping/instancing, the camera controller -- and finally the Sampler. + +## Modeling-app feature layer (first tier): selection + search + tagging on the query layer (+8 tests) + +The backlog's "biggest organizational win per line," and the clearest demonstration of the reframe: the scene is a +VSA table of object records, so the whole organizational layer is query/bundle/cleanup in a DCC costume, no new +machinery. holographic_scene_query.py, on the canonical Scene document (item 0): + +* SELECTION = a query result (a set of handles). select(scene, name/name_contains/material/tag/has_tag/where) is + plain, readable, EXACT filtering over the stored records (fast, lossless). select_by_tag is a tag query. +* SEARCH can be FUZZY: select_fuzzy(scene, role, value) rides holographic_query's Table -- ranks objects by how + well a property MEANS the value (cosine of the unbound filler to the probe) and returns [(handle, confidence)] + with the confidence SURFACED (a silent near-miss is a footgun). KEPT NEGATIVE: with random-atom fillers this is + exact-match-with-confidence; true 'metal'~'steel' nearness needs a meaning-bearing value encoder (separate). +* SET ALGEBRA (Selection.union/intersect/minus/invert) is the algebra of selections -- "everything metal, minus + the front wheel" is one line. NAMED sets save membership as an ID LIST (exact, no capacity limit) -- NOT one + giant bundle (the decode ceiling). as_bundle() offers the holographic "selection as a hypervector" for the + vector-algebra case, with that ceiling noted (a member reads as present by high cosine). +* TAGGING = a bound role written through scene.edit / the new scene.remove_tag, so tags are queryable AND get undo + + change events for free. + +MEASURED (selftest + 7 pytests + integration): exact select by material/tag/substring/predicate; set algebra +composes (metal minus front-wheel = rear-wheel); named sets save + apply to the document's selection; fuzzy select +ranks the metal parts with calibrated confidence in [0,1]; tag/untag write through the document (undo removes a +tag, edit event fires); a member reads as present in the selection bundle; deterministic. + +FACULTIES (default-off): selection, select_objects. Also added Scene.remove_tag (edit only MERGES tags; this is the +delete, with undo+notify). Integration: query metal -> save named set -> apply to selection -> fuzzy select with +confidence -> tag + undo, one flow through the mind. + +Modeling-app backlog progress: foundation (item 0, C+D, F+G+H) + the first feature tier (selection/search/tagging) +done. Next per sequencing: the undo/redo stack (a thin wrapper on the deltas the Scene already records), then +measurement/units (quantities), render overrides (bound role with fallback), snapping (cleanup), grouping/ +instancing, the camera controller, and finally the Sampler. + +## Modeling-app feature layer: the undo/redo STACK -- grouped transactions, labels, history (+7 tests) + +The thin item the foundation set up: item 0's Scene already recorded a reversible before/after snapshot on every +mutation, so undo was already there. This adds the user-facing STACK on top, entirely inside holographic_scene_doc.py +(owned by the Scene, backward-compatible -- basic undo/redo behaviour unchanged, verified by the existing tests): + +* _UndoStep(label, changes): a step is now a LABEL + a batch of (handle, before, after) snapshots, applied together. + A single mutation is a one-change step; a transaction coalesces many mutations into ONE step. +* TRANSACTIONS: begin_group(label)/end_group() and a `with scene.group(label):` context manager -- everything inside + becomes one undo step, so a drag or a multi-object tool is a SINGLE undo (not a hundred). Nesting commits once + (outermost label); an empty group records nothing. +* LABELS + HISTORY: each mutation auto-labels ("Add wheel", "Edit body", "Remove ...", "Untag ..."); history() / + redo_history() give the Edit-menu / history-panel content; can_undo()/can_redo(). +* Depth cap (_max_undo, default 200): drop the oldest step past the cap. Redo invalidated by a fresh edit. + +undo()/redo() now apply a whole step (restore all before/after snapshots; reverse order on undo). Identity atoms are +never touched, so handles stay valid across grouped undo/redo. + +MEASURED (selftest + 6 new pytests + integration): a group of 3 edits is one labelled step and one undo reverts all +three (redo re-applies); history() lists the labels; nested groups commit once and one undo reverts both; the depth +cap keeps only the most recent N; can_undo/can_redo track state and a fresh edit clears redo; an empty group adds +nothing. Integration: select the metal parts, re-material the whole selection inside one transaction, a SINGLE undo +reverts the batch (the untouched object unaffected) -- the stack riding the document + selection. + +KEPT NEGATIVES (loud): still a RECORD-level snapshot swap (transform + small dicts + geometry-by-reference), O(one +record) per change -- a geometry-heavy in-place edit wants scenedelta's O(change) geometry delta; this composes with +it, doesn't replace it. No automatic coalescing of separate consecutive edits (slider drag) -- use an explicit +transaction, which is the robust, readable mechanism. The depth cap drops old steps silently (standard for an undo +history). + +Modeling-app backlog progress: foundation (item 0, C+D, F+G+H) + feature layer so far (selection/search/tagging, the +undo/redo stack). Next per sequencing: measurement + units (quantities), render overrides (bound role with +fallback), snapping (cleanup), grouping/instancing, the camera controller, and finally the Sampler. + +## Modeling-app feature layer: measurement + units, from geometry (+8 tests) + +The measuring tool. holographic_metrology.py (named "metrology" because holographic_measure.py is already the +statistical variance harness -- a name collision, unrelated). Two disciplines the backlog insists on, both honoured: +measure from the ACTUAL geometry (never a lossy VSA readback), and return a DIMENSIONED quantity. + +* Every function walks the mesh vertices/faces directly and wraps the result in holographic_quantities.Quantity, + which carries the unit AND the 8-D dimension exponent vector: surface_area -> [m^2] (sum of triangle areas, + winding-independent), volume -> [m^3] (divergence theorem, (1/6)|sum v0.(v1 x v2)|), bounding_box (a BBox with + extents + diagonal as [m]), centroid (area-weighted), distance/edge_length -> [m], angle_at / dihedral_angle / + angle_between (radians, dimensionless), degrees() helper. +* CONVERSION is one multiply (q.to("ft"), q.to("L")) -- from the Quantity type, no factor to fumble. +* The DIMENSIONAL ALGEBRA refuses nonsense: adding a length to an area, or expressing a volume as an area, raises a + grammar error loudly -- so a measurement bug can't produce a silent meaningless number. + +MEASURED (selftest + 7 pytests + integration): a unit cube measures area 6 m^2, volume 1 m^3, bbox diagonal sqrt(3) +m, centroid at the centre; 1 m^3 = 1000 L and a 1 m edge = 3.2808 ft by one multiply; area+area = 12 m^2 but +area+length and volume.to("m2") both RAISE; a cube corner is a 90-degree right angle (angle_at and dihedral); +deterministic. + +FACULTIES (default-off): measure_area, measure_volume, measure_bbox, measure_distance. Integration: a Scene object +carries a real cube mesh; measuring it through the mind returns dimensioned area/volume/diagonal (convertible) and +the dimensional algebra refuses area+length -- the measuring tool riding the Scene document. + +KEPT NEGATIVES (loud): areas and lengths are exact for the mesh as given (no assumptions); VOLUME assumes a CLOSED, +consistently-wound surface -- on an open or inconsistently-wound mesh it is meaningless, flagged in the docstring +not hidden. Angles are dimensionless radians (a radian is a ratio), returned as plain floats. The centroid is the +SURFACE (area-weighted) centroid, not the solid centroid -- a deliberate, documented choice. + +Modeling-app backlog progress: foundation (item 0, C+D, F+G+H) + feature layer so far (selection/search/tagging, the +undo/redo stack, measurement/units). Next per sequencing: render overrides (a bound role with fallback), snapping +(cleanup), grouping/instancing, the camera controller, and finally the Sampler. + +## Modeling-app feature layer FINISHED: overrides, snapping, grouping/instancing, camera controller (+25 tests) + +The rest of the feature layer, each feature falling out of the VSA reframe, all riding the canonical Scene document. + +**Render overrides = a bound role with fallback (holographic_overrides.py).** A per-object render setting is a bound +role on the record; resolving is bind-with-fallback: object override -> material override (if the material is a +dict) -> scene default -> bare default. Only DELTAS are stored (a scene of thousands with two special objects costs +two entries), everything else inherits. Added a first-class `overrides` dict to SceneObject (parallel to tags/params, +merged by edit, snapshotted for undo) + Scene.clear_override (like remove_tag). resolve/set_override/clear_override/ +effective_settings/overridden_props. Faculties: resolve_override, set_override. + +**Snapping = cleanup (holographic_snap.py).** Snapping projects a dragged continuous position onto the nearest +ALLOWED place -- grid node, vertex, edge point, angle increment -- exactly as cleanup projects a noisy vector onto +the nearest atom, and with the same confidence gate: a TOLERANCE, so if nothing is close enough the point is left +alone (index -1). snap_to_grid/snap_to_points(=cleanup)/snap_to_segment/snap_value/snap_angle + a Snapper (grid + +vertices, vertex beats grid). Reads raw coordinates (honest, exact). Faculty: snapper. + +**Grouping = a bundle, instancing = a bind (holographic_grouping.py).** A group is a null parent owning its members +(hierarchy), identity = a BUNDLE of member atoms (a member reads as present); ONE undo step; ungroup re-parents + +drops the null. An instance SHARES one source geometry (read through the source handle) with its OWN transform -- the +geometry is the shared filler, the transform the bound placement -- so editing the source updates all instances +(nothing copied). group_objects/ungroup/group_members/group_bundle/instance/resolve_geometry/instances_of. +Faculties: group_objects, instance. + +**Camera controller (holographic_camera.py).** Viewport navigation on the item-G transform utilities: orbit +(turntable, distance-preserving, pole-clamped), pan (eye+target together), dolly (no target overshoot), zoom (scale +radius), frame (fit a box's bounding sphere, distance = radius/sin(fov/2)), view_matrix (look_at) + to_camera. +Faculty: camera_controller. + +MEASURED (4 selftests + 22 pytests + 3 integration): override fallback + delta-only storage + undoable clear + +material tier; grid/point/segment/value/angle snap + tolerance-gated refusal + vertex-beats-grid; group-is-one-undo ++ bundle recognizes a member + instance shares+follows the source; orbit preserves distance & wraps at 360 + +elevation clamp + pan/dolly/zoom/frame + view looks at target. Integration: override+snap, group+instance+source-edit, +and camera-frames-a-measured-bbox, all through the mind. + +HIERARCHY REFACTOR (needed for undoable grouping): moved the parent from a separate scene.parent map ONTO the record +(SceneObject.parent), so re-parenting is an ordinary snapshot-undoable edit; set_parent now records undo; added +Scene.parent_of; children_of iterates objects. Backward-compatible (full suite green). + +BUG FIXED (caught only by running the FULL suite, per the run-the-whole-suite discipline): my physics-backbone +`spectral_field` faculty (added in the physics backlog) COLLIDED with the pre-existing fractal-volume +`spectral_field(shape, beta, seed)` -- the later def shadowed the earlier, breaking a fractal-volume integration +test that I'd never re-run. Renamed the physics faculty to `spectral_pde` (no test/tour used it via the mind); the +established fractal API keeps its name. Full suite now green. + +KEPT NEGATIVES (loud): overrides -- the material tier only applies when the material is a dict carrying overrides +(a named-material registry would generalize it). Snapping reads raw coords (no VSA encoding) -- exact, the honest +choice for something this precise. Instancing shares geometry only; per-instance material/transform are independent +but a per-instance geometry TWEAK would break the share (by design -- that's a real copy). Camera orbit clamps +elevation at +/-89 degrees (a turntable, not a free-fly 6-DOF camera -- a deliberate scope). + +Modeling-app backlog: the FOUNDATION (item 0, C+D, F+G+H) and the FEATURE LAYER (selection/search/tagging, undo/redo +stack, measurement/units, render overrides, snapping, grouping/instancing, camera controller) are DONE. The only +remaining item is the Sampler -- and the modeling-app backlog file has rotated out of uploads, so its exact spec +needs re-confirming before building it (flagged to the user rather than guessed). + +## Modeling-app backlog CAPSTONE: the Sampler -- a placeable read-probe (+8 tests) -- BACKLOG COMPLETE + +The last item, and the clearest single example of the "think holographically" payoff: a brand-new, genuinely useful +object that costs almost nothing because it's the READ-DUAL of a FieldEffect. holographic_sampler.py. + +A FieldEffect is a shaped, falloff-weighted WRITE to a field; a Sampler is the SAME shape + _falloff + attach +pointed the other way -- a shaped, falloff-weighted READ from the scene. It's a Scene object (handle + transform + +shape + mode + target), placed/moved/animated like anything else. Three modes, each reusing a piece already in the +box: POINT (read the value at one spot), SURFACE (emit_from_surface points on a patch, read + weight each), VOLUME +(fill the shape interior where sdf<0, read + weight + aggregate -- weighted mean = a bundle, sum = a MC integral). + +OVERLAP is the holographic move: when several objects cover the region, tag each sample by its owning object's +STABLE HANDLE atom (near-orthonormal), scale the handle by that sample's contribution, and bundle -> ONE superposed +readout that is SEPARABLE (an object's share = dot with its handle) and COLLAPSIBLE (total = sum of shares), with +the dominant contributor a cleanup (argmax over the handle codebook). owners_from_sdfs builds the per-point owner +labels (nearest object's SDF). place_sampler drops it into the Scene; sampler_triggers gives the threshold trigger. + +MEASURED (selftest + 7 pytests + integration): point reads the field at its spot; volume averages the interior (~0 +over a symmetric region, >0 shifted to the upper half); the labeled bundle separates two objects' shares by handle, +names the dominant, and collapses to the total; placeable in the Scene; a trigger crosses a threshold; deterministic. +Integration: two objects with SDF geometry, a volume sampler labeled by the Scene's OWN stable handle atoms +(handle_vector, item 0/B) -- each contribution recovered by its handle, dominant a cleanup, plus a placed point probe. + +FACULTIES (default-off): sampler, place_sampler. + +KEPT NEGATIVES (loud): the labeled bundle here is the SCALAR-contribution form (contribution x handle, summed) -- +exact for near-orthonormal handles; a VECTOR-valued field read generalizes via bind(handle, value_vec) + unbind (the +backlog's general form), not built. Volume fill is uniform random rejection-sampled inside the shape (Monte-Carlo), +so the integral has MC variance ~1/sqrt(n) -- Poisson-disk / blue-noise (lowdiscrepancy) would lower it; the honest +baseline is plain MC. owners_from_sdfs uses nearest-SDF ownership (argmin distance), which is the surface/inside +heuristic, not a watertight CSG membership test. + +=== MODELING-APP SUPPORT BACKLOG COMPLETE === +The whole backlog is done: the ARCHITECTURE layer (item 0 canonical Scene + stable handles + change notification; +C+D recipe = modifier stack + dependency graph + property introspection) and the FEATURE layer (cancellation, +transform utilities, the API facade; selection/search/tagging; the undo/redo stack; measurement/units; render +overrides; snapping; grouping/instancing; the camera controller) and the CAPSTONE Sampler. The compounding the +backlog predicted held: the query layer gave selection+search+tag; the delta/snapshot store gave undo/redo; quantities +gave units+measurement; bound-role-with-fallback gave overrides; cleanup gave snapping; the transform controller gave +the camera; bundle/bind gave grouping/instancing; and the Sampler fell out as FieldEffect's read-dual. The app's spine +is the VSA table + the recipe, not a dozen bespoke systems. Also this span: fixed the spectral_field faculty collision +(-> spectral_pde), moved the hierarchy onto the record (undoable parenting), and added pyproject.toml so the README's +pip extras work. + +## Inverse-rendering backlog STARTED: IR1 auto-bump -- image -> height -> normal map -> material channel (+9 tests) + +First item of the new inverse-rendering & auto-material backlog (saved into the tree as +holostuff_inverse_rendering_backlog.md so it can't rotate out again). Re-audited the backlog's "already built" +claims against CURRENT live code (probe-first): vision.to_gray/sobel/gradient, displace.bump_normals (the MESH +path), the Material role-filler channel record (texture_field encodes a scalar UV channel), octnormal quantization, +and fluid's FFT-Poisson project() (for IR7) all confirmed present. The genuinely-new code was small, as the audit +said: an image->height ESTIMATOR (high-pass) + the 2D normal-from-height map + an honest confidence/abstain gate. + +holographic_autobump.py: grayscale -> HIGH-PASS (image minus a separable Gaussian blur, so a slow lighting/albedo +ramp does NOT become a fake slope) = the height; normal_from_height = normalize(-s*dh/dx, -s*dh/dy, 1) (the standard +grayscale-to-normal) -> a unit (H,W,3) tangent-space normal map; bump_confidence = std of the INTERIOR high-pass +(border cropped to skip reflect-padding edge artifacts); auto_bump gates on it (abstain -> flat when too little +detail); quantize_normals reuses octnormal; add_height_channel encodes the height into a Material height channel +(which displace consumes for IR5). Faculty: auto_bump. + +MEASURED (selftest + 8 pytests + integration): a slow ramp's interior high-pass std ~0.002 (removed) vs a bumpy +pattern ~0.18 (~16x) -> the ramp ABSTAINS, the bump gives unit normals that vary (relief) at confidence 0.18; flat +height -> flat normals; packed RGB in [0,1]; octnormal round-trips to unit normals; the height wires into a Material +and samples back; deterministic. Integration: auto-bump a bumpy albedo through the mind -> unit normal map, abstain +on a flat image, wire the height into a Material carried by a Scene object. + +KEPT NEGATIVES (loud): luminance-as-height is a HEURISTIC, not a measurement -- baked directional light becomes fake +grooves (a cast shadow = a crevice), albedo that isn't height becomes fake relief (a painted stripe = a ridge), a +fundamental albedo/relief ambiguity no arithmetic resolves (that's IR2 light-aware + IR8 photometric stereo, both +bounded). bump_confidence measures DETAIL PRESENCE, not relief-vs-albedo: it stops us inventing relief from a +featureless image, it does NOT verify the detail is relief -- a busy printed poster WILL read as relief. A plausible +perceptual bump, not a depth map; `strength` is a user knob, not an inferred scale. High-pass has reflect-padding +border artifacts (why the confidence gate crops the border). + +Next per the backlog's §5 sequencing: IR7 (FFT height<->normal integration via fluid's FFT-Poisson solve -- +consistent + seamless-tileable, "do with IR1"), then ST1 (colour transfer, tiny), IR5 (displacement from a confident +height), then the IR4 render->compare->adjust loop (the headline; needs a render-vs-target metric + a gradient-free +loop on pipeline.py). + +## Inverse-rendering IR7: surface-from-gradient by FFT (Frankot-Chellappa) -- consistent, tileable height (+7 tests) + +The inverse of IR1's height->normal, done "with IR1" per the sequencing. holographic_surfaceint.py. Recover a +single-valued CONSISTENT height from a (generally non-integrable) normal/gradient field. Frankot & Chellappa (1988): +the least-squares integrable height is pure FFT -- one forward transform of the gradients, a per-frequency divide, +one inverse (Z_hat = (-j*wx*P_hat - j*wy*Q_hat)/(wx^2+wy^2), DC=0) -- the same FFT-on-a-periodic-domain operator a +bind and the fluid solver use. gradient_from_normals (p=-nx/nz, q=-ny/nz), height_from_gradient (the FC solve), +height_from_normals (compose), consistent_normals (project a non-integrable map onto the nearest integrable one). +Faculty: integrate_normals. + +MEASURED (selftest + 6 pytests + integration): recovers a known periodic height from its ANALYTIC gradient at +correlation 1.0000 (<2% RMS) and from a NORMAL map at 0.99+ (finite-diff derivative, looser); gradient_from_normals +matches np.gradient; the result is periodic so it TILES seamlessly (seam ~ interior scale); re-deriving normals from +the integrated height reproduces the height (integrable); deterministic. Integration: auto-bump (IR1) -> normal map +-> integrate (IR7) -> consistent tileable height, round-trips through the mind. + +KEPT NEGATIVE (loud): the periodic boundary is a SYSTEMATIC BIAS on a NON-periodic surface (opposite borders forced +to agree -- the textbook FC artifact). For a tileable MATERIAL that periodicity is a FEATURE; for a bounded scene +surface, prefer a DCT/DST variant (Simchony 1990) or a Poisson solve with the real boundary -- state the regime. +The normal-map round-trip is slightly looser than the analytic one because IR1 uses a FINITE-DIFFERENCE derivative +while the FC solver inverts the SPECTRAL derivative (a small operator mismatch, exact only for band-limited signals). + +Inverse-rendering progress: IR1 (auto-bump) + IR7 (FFT integration) done -- the auto-material front is consistent and +tileable. Next per §5: ST1 (colour transfer, tiny -- match a reference image's mean/covariance, a postfx add), then +IR5 (real displacement from a confident height via displace), then the headline IR4 render->compare->adjust loop +(the real gaps: a render-vs-target metric + a gradient-free loop as Stages on pipeline.py). + +## Inverse-rendering ST1: colour transfer -- grade toward a reference's statistics (Reinhard 2001) (+7 tests) + +The easy, powerful grading win. holographic_colortransfer.py. Match a reference image's colour STATISTICS onto +another image -- the "make this render feel like that sunset photo" knob. Pure statistics, no learned weights. Two +modes: 'meanstd' (match each channel's mean+std -- simplest Reinhard, ignores cross-channel correlation) and +'covariance' (match the full mean + 3x3 covariance by WHITENING the source and COLOURING by the reference -- the +Monge-Kantorovich linear transfer, handles correlated grades like teal-orange). Matrix square roots via eigh of the +(SPD) covariances, eigenvalue-clamped so a grayscale/degenerate covariance stays stable. strength in [0,1] blends +original->transfer. Faculty: color_transfer. + +NAMING (the audit's §B collision, avoided): postfx.reinhard is the TONEMAP (HDR->LDR, x/(1+x)); this reference-based +statistical transfer is a DIFFERENT function -- kept in its own module, slots into the postfx grade stage. + +MEASURED (selftest + 6 pytests + integration): covariance mode makes the output's mean AND full covariance match the +reference (atol 1e-3 on cov); meanstd matches per-channel mean/std; strength=0 is the identity, 0.5 sits exactly +between original and full; shape preserved; clips to [0,1]; a degenerate grayscale covariance stays finite; +deterministic. Integration: grade a cool bluish render toward a warm reference through the mind -> takes the +reference's mean/mood; no-op at strength 0. + +KEPT NEGATIVE (loud): GLOBAL statistics only -- it moves COLOUR, not content, and WASHES OUT when the palettes are +very different (a linear map can't turn a green field into a red desert without destroying detail). Local / +histogram-matching variants fix this at more cost. + +Inverse-rendering progress: IR1 (auto-bump) + IR7 (FFT integration) + ST1 (colour transfer) done. Next per §5: IR5 +(real displacement from a confident height -- small reuse of displace.displace_mesh, rides on IR1's height), then the +headline IR4 render->compare->adjust loop (the real gaps: a render-vs-target metric -- SSIM-ish, vision has only the +ingredients -- and a gradient-free optimise loop as Stages on pipeline.py). + +## Inverse-rendering IR5: displacement from a confident height -- bump -> real geometry, gated (+7 tests) + +Pure reuse of the shipped displace operator, gated on IR1's confidence. holographic_autodisplace.py. A bump map +(IR1) only tilts SHADING normals -- the silhouette/grazing profile stay flat; IR5 promotes a HIGH-confidence height +to REAL geometry by moving each vertex along its normal by amount*height(uv), via displace_mesh. Only genuinely-new +code: the WIRING (a bilinear uv->height sampler; planar uvs for a mesh without any) + the CONFIDENCE GATE. +displace_from_height (gated; abstain -> mesh unchanged), auto_displace (auto_bump -> displace if confident enough +for geometry, a stricter threshold than a shading bump). Faculty: auto_displace. + +MEASURED (selftest + 6 pytests + integration): a confident bumpy height moves a flat grid's vertices into real +relief (max z 0.30), the centre (high height) rising more than a corner (low) -- relief follows the height; a +low-confidence height ABSTAINS and leaves the mesh flat (z all 0); bilinear samples the four corners + centre-mean +correctly; auto_displace applies on a bumpy image, abstains on a flat one; deterministic. Integration: a Scene +object's flat grid mesh auto-displaced from a bumpy albedo gains relief (stored back on the object), a flat albedo +leaves it flat. + +KEPT NEGATIVE (loud): displacement is only as good as the height estimate -- it inherits ALL of IR1's ambiguities +(a cast shadow becomes a REAL groove, a painted stripe a REAL ridge), and adds real geometry cost. It is gated +HARDER than a shading bump precisely because the failure is worse when it moves vertices than when it only tilts a +normal. + +Inverse-rendering progress: IR1 (auto-bump) + IR7 (FFT integration) + ST1 (colour transfer) + IR5 (displacement) done +-- the whole auto-material track (bump, integrate, grade, displace) is up. Next per §5 is the HEADLINE: IR3 (the +perception->scene-hypothesis bridge) -> IR4 (the analysis-by-synthesis render->compare->adjust loop). The real IR4 +gaps the audit named: a render-vs-target COMPARE METRIC (vision has histograms/edges but no SSIM-style image compare +-- the ingredients, not the metric) + a GRADIENT-FREE optimise loop built as Stages on the shipped pipeline.py. + +## Inverse-rendering IR4 (part 1): perceptual render-vs-target compare metric (+8 tests) + +The first of IR4's two real gaps (the audit: vision has histograms/edges -- the ingredients -- but no image-compare +metric). holographic_imagecompare.py. The analysis-by-synthesis loop needs a render-vs-target objective that is NOT +raw pixel MSE (a one-pixel shift or a tiny exposure change wrecks MSE while the images look identical). So it is +PERCEPTUAL, built from three shift/lighting-tolerant pieces: multi-scale SSIM (Wang 2004 -- local luminance/contrast/ +structure over Gaussian windows, reusing autobump.gaussian_blur), per-channel colour-histogram intersection (shift- +invariant palette match), and normalized correlation of gradient-magnitude maps (edge alignment). Combined into +perceptual_similarity in [0,1] and perceptual_distance = 1 - it (the loop's objective). Faculties: compare_images, +image_distance. + +MEASURED (selftest + 7 pytests + integration): identical scenes score 1.000 (distance 0); a 2px shift of a scene +stays perceptually similar (0.90) and ranks clearly above a different scene (0.81); on TEXTURED content a shift's MSE +is ~88% of a different image's, so MSE is nearly blind to the shift-vs-different distinction the perceptual metric +makes; a brightness offset barely moves SSIM (0.98, structure preserved); symmetric; components in range; +deterministic. Integration: through the mind, a nudged scene ranks closer to the target than a clearly-different +scene, and is a confident match (>0.85) -- the render-and-compare ranking the loop needs. + +KEPT NEGATIVE (loud): the ceiling is roughly SSIM-quality STRUCTURAL comparison, NOT a learned LPIPS-style perceptual +loss (that needs trained weights the constitution bans). A good, deterministic render-and-compare objective, not +human perception. Also: random scenes with the same sky gradient can be genuinely similar -- the metric was RIGHT to +rank a too-similar "different" scene near a small shift (caught in the integration test; fixed by using a +deterministically-different reddish scene). SSIM with a Gaussian window still degrades under a large shift; the +colour term (fully shift-invariant) carries most of the small-shift robustness. + +Inverse-rendering progress: auto-material track done (IR1/IR7/ST1/IR5); IR4 part 1 (the compare metric) done. Next: +IR4 part 2 -- the GRADIENT-FREE optimise loop (coordinate descent / sampled population) that adjusts camera + sun +direction to MINIMIZE image_distance, gated by conformal accept/abstain, built as Stages on pipeline.py -- plus IR3 +(the perception->hypothesis warm-start). The measurable milestone is the SELF-RECOVERY test: render a known scene, +recover its Camera + Light.direction within tolerance. + +## Inverse-rendering IR4 (part 2, THE HEADLINE): analysis-by-synthesis loop -- self-recovery (+7 tests) + +The auto-calibration loop, the backlog's headline. holographic_inverserender.py. Given a TARGET image, recover the +scene parameters that reproduce it -- the CAMERA (turntable az/el/radius) and the SUN direction (az/el) -- by +render -> compare -> adjust: render a hypothesis (shipped render_sdf, ao/shadows off = ~4ms/frame), compare with the +IR4-part-1 perceptual distance (NOT MSE), adjust GRADIENT-FREE. Autodiff is banned (exactly why differentiable +renderers can't be borrowed), so the optimizer is a readable COMPASS/PATTERN SEARCH (try each param +/- a step, move +to the first improving neighbour, shrink the step when stuck). A conformal-style accept/abstain gate calibrates "what +a good match looks like" on tiny perturbations of the truth and gates the recovered distance. Faculty: recover_scene. + +MEASURED (selftest + 6 pytests + integration): SELF-RECOVERY milestone -- render a known box, recover from a +perturbed warm start: perceptual distance 0.412 -> 0.003 (99% down, ~97 evals, ~0.7s), camera az/el/radius recovered +to |err| (0.00, 0.01, 0.00) and sun to (0.01, 0.00) -- essentially exact; the gate ACCEPTS the good match (threshold +0.192) and ABSTAINS on an unmatchable target (a sphere the box hypothesis can't become, dist 0.227 > 0.192); +deterministic. Integration: self-recovery through the mind, gate accepts, camera+sun within tolerance. + +KEPT NEGATIVES (loud): (1) gradient-free is COARSER and SLOWER than differentiable inverse rendering and leans on a +decent WARM START (IR3's perception seed / analog recall) to land in the right basin -- from a wild init it can stall +in a local minimum. (2) The objective ceiling is the perceptual metric's (SSIM-structural, not learned LPIPS). (3) It +matches the VISIBLE FRAME; occluded geometry and absolute metric depth are not recoverable (IR6) -- it recovers the +viewpoint and light, not the unseen back of the object. + +NOTE on pipeline.py: the backlog suggested building the loop as Stages on pipeline.py, but that Stage system orders a +SINGLE render pass's G-buffer (needs/produces) -- an iterative optimize LOOP naturally wraps render passes rather than +being one. Built the loop as a clean readable optimizer that drives the shipped renderer each step (the honest fit); +render_params is effectively the "render stage" it calls. Kept the design note rather than forcing the loop into the +Stage/G-buffer frame. + +Inverse-rendering progress: auto-material track (IR1/IR7/ST1/IR5) + IR4 (compare metric + the analysis-by-synthesis +loop, self-recovery milestone MET) done. Remaining per §5: IR3 (perception->hypothesis warm-start: vision features -> +semantic scene seeds + analog recall -- would replace the hand-perturbed init with a real warm start), IR12 (FSR1 +upscaler), IR10/IR13 (SVGF audit says already built; checkerboard), ST2/ST3 (example-based texture/super-res), IR11 +(3D splat-bundle archive), IR14 (render channels/AOVs = expose unbind), IR2/IR8/IR9 (light-aware/photometric/intrinsic, +opt-in), IR6 (a boundary, not a build). + +## Full-suite run caught a PRE-EXISTING faculty collision: UnifiedMind.explain shadowed (fixed) + +Running the FULL suite (in shards, to fit the wall clock) after the inverse-rendering work surfaced ONE failure -- +not from this span's code, but a latent collision the per-turn testing never hit: UnifiedMind had TWO `def explain(` +methods -- the established record-compare `explain(x1, x2)` (line 856, used by relations + unified tests to explain +the difference between two records/entities) and a newer program dry-run `explain(machine, program_vec)` (line 6225, +Query Interface Phase 7). Python keeps the LAST definition, so the program-explain SHADOWED the record-explain, and +`m.explain("france","belgium")` was dispatching into the program path -> AttributeError ('dict'/'str' has no .run()). +Three tests were silently red (relations + two in unified) and only turned up under the full suite -- exactly the +spectral_field lesson again: per-turn new-code testing misses a shadow of an OLD faculty. + +FIX (backward-compatible): renamed the newer program dry-run to `explain_program` (matching its underlying +holographic_query.explain_program), updated its 2 call sites (test_integration + tour). The established `explain(x1,x2)` +is no longer shadowed. All three previously-red tests pass; the program-explain integration test passes with the new +name. Not this span's regression, but found and fixed here. + +FULL SUITE after the fix: 2712 passed, 17 skipped, 0 failed (reconciles to the 2729 collected). Lesson reinforced: +run the full suite when adding faculties -- a new (or newly-noticed) method can shadow an old one and only the whole +suite reveals it. + +## Inverse-rendering IR3: perception -> hypothesis bridge -- analog-recall warm start for IR4 (+9 tests) + +The front-end that seeds the IR4 loop, closing analysis-by-synthesis end to end. holographic_perception.py. IR4 is +gradient-free so it needs a decent warm start; IR3 reads it off the target image (probe-first confirmed the vision +features exist: dominant_colours, hough_lines, to_gray; HoloForest.recall with_agreement for the sublinear recall). +Pieces: scene_descriptor (unit-norm coarse luminance-layout grid + mean RGB -> HoloForest-indexable), +estimate_light_direction (COARSE sun from the brightness-weighted centroid -- bright region's x->azimuth, y-> +elevation), scene_hypothesis (archetype gist: palette + horizon row + sun), SceneLibrary (analog recall over a small +exemplar library; warm_start returns the nearest scene's params + the forest's cross-tree AGREEMENT as an abstain +signal). Faculties: scene_hypothesis, estimate_light_direction. + +MEASURED (selftest + 8 pytests + integration): descriptor unit-norm + deterministic; sun estimate points to the +bright side (az 0.93 for a right blob; left<-0.2; top>bottom in elevation); horizon split found near the sky/ground +boundary; analog recall finds the nearest library scene (agreement 1.00) and its params warm-start IR4 to distance +0.002, recovering camera/light err (0.01, 0.03) FROM A PERCEIVED START -- no hand-perturbation; abstains on low +agreement. Integration: full IR3->IR4 through the mind -- perceive (sun + analog warm start) then refine to recover +the camera + sun from the image alone. + +KEPT NEGATIVES (loud): ARCHETYPE-level recall, not semantic segmentation -- works inside the library's vocabulary, +ABSTAINS (low cross-tree agreement) rather than hallucinating outside it. The sun-from-luminance cue is COARSE (a +bright albedo patch reads as 'sun this way'), a start for IR4 to refine, not a measurement. + +INVERSE-RENDERING BACKLOG -- scene-matching headline COMPLETE: IR3 (perception/warm-start) -> IR4 (compare metric + +gradient-free loop, self-recovery). Auto-material track also complete (IR1/IR7/ST1/IR5). Remaining are the +lower-priority / assembly items: IR14 (render channels = expose unbind), IR12 (FSR upscaler), IR13 (checkerboard), +IR10 (SVGF -- audit says already built, verify), ST2/ST3 (example-based texture/super-res), IR11 (3D splat-bundle +archive), IR2/IR8/IR9 (light-aware/photometric/intrinsic, opt-in), IR6 (a boundary, not a build). + +## Inverse-rendering IR14: render channels / AOVs -- a channel is an unbind (+7 tests) + +Mostly exposure, as the audit said. holographic_renderchannels.py. Selectable, separate render passes each with its +own alpha, for compositing/science/debug; default (no selection) = the beauty pass, BIT-IDENTICAL to render_sdf. The +holographic reading: a render channel IS AN UNBIND, the scene a bundle at every level -- so the data/object/material +passes are a view over decompose the engine already runs. render_channels: DATA/G-buffer (depth/normal/position/mask +from one shipped sphere_trace), OBJECT (Cryptomatte-style per-object coverage matte = nearest-object argmin at each +hit). material_channels = material.channel() per name (the literal unbind). composites_to_beauty checks the +compositor invariant. Faculty: render_channels. + +MEASURED (selftest + 6 pytests + integration): default is beauty-only, bit-identical to render_sdf; G-buffer passes +valid (unit normals + positive depth at hits); per-object mattes composite back to the coverage EXACTLY (err 0.0) and +are DISJOINT (no gaps, no double-count -- the Cryptomatte "passes must add up" invariant); material channels recover +by unbind (cosine >0.4, crosstalk-carrying); selecting a subset returns only what's asked (opt-in per channel); +deterministic. Integration: through the mind, beauty default bit-identical + two-object mattes composite back. + +KEPT NEGATIVES (loud): the LIGHTING passes (direct/indirect/diffuse/specular/GI/shadow) are the ONE genuinely-new bit +and are NOT in v1 -- the path tracer averages over ALL paths, so splitting them needs trace-time accumulation of a +labelled buffer per contribution, and summing them exactly to beauty needs care at the MIS/Russian-roulette +boundaries (scoped out, named). material.channel() carries capacity CROSSTALK (fine for matte/debug, use +material.sample for exact re-lighting values). N channels = N buffers -> opt-in per channel, never all-on. DEEP +compositing (many depth samples/pixel) is out of scope for v1. + +Inverse-rendering progress: auto-material (IR1/IR7/ST1/IR5) + scene-matching headline (IR3->IR4) + IR14 (render +channels) done. Remaining: IR12 (FSR upscaler), IR13 (checkerboard), IR10 (SVGF -- audit says already built as +holographic_svgf.py, VERIFY), ST2/ST3 (example-based texture/super-res), IR11 (3D splat-bundle archive), IR2/IR8/IR9 +(opt-in), IR6 (a boundary, not a build). + +## Inverse-rendering IR10 (SVGF): VERIFIED ALREADY COMPLETE (probe-first, no new code) + +Probe-first audit per the backlog: IR10's SVGF 1-spp denoiser is holographic_svgf.py and is FULLY closed out -- +atrous_bilateral (feature-cosine edge-stopping bilateral over the a-trous hierarchy, the engine's-way SVGF: bind +(normal, albedo, depth) per pixel, blend by cosine, RBF falloff = ScalarEncoder bump, dilation = multires pyramid), +a test file (test_holographic_svgf.py, green), the svgf_denoise faculty on UnifiedMind (line 6269), and tour coverage. +Selftest MEASURED: noisy 17.1 dB -> 41.2 dB, beating the edge-blind blur (19.0 dB); edge MSE 0.0001 vs blur 0.0572; +deterministic. Its docstring already carries the probe-first note (siblings robust_accumulate/SPRTRecall/TemporalReuse/ +multires are shipped) and kept negatives (denoises, doesn't add detail; the bound-feature cosine must be MEASURED not +assumed). No work needed -- documented as done, count unchanged. The audit's claim confirmed. + +## Inverse-rendering IR12: FSR1-style upscaler -- EASU + RCAS (+8 tests) + +Half already shipped, as the audit said. holographic_fsr.py. FSR1 = EASU (edge-adaptive upsampling) + RCAS (noise- +aware sharpen). RCAS IS holographic_postfx.sharpen already (Van Cittert, kept-negative "stop at the noise floor" = +RCAS's design goal) -- wired as the second pass, not rebuilt. The genuinely-new piece is EASU: a separable Lanczos-2 +upscale (Duchon 1979, FSR1's EASU base -- sharper than the plain bilinear postfx.resample it exists to beat) with an +ANTI-RINGING clamp (each output bounded to its low-res 3x3 neighbourhood min/max, killing Lanczos overshoot exactly +where gradients reverse = edges). lanczos_upscale, easu_upscale, fsr_upscale (easu -> sharpen). Faculty: upscale. + +MEASURED (selftest + 7 pytests + integration): on a 2x downscale->upscale round-trip, EASU beats bilinear on +PSNR-to-native (20.52 vs 18.82 dB on a stripe/block image; 28.79 vs 28.70 on a smoother render) and on edge sharpness; +anti-ringing holds it in [~0,1] (no overshoot); RCAS adds crispness over EASU; a flat image is preserved (partition of +unity); deterministic. Integration: upscale a low-res render through the mind, EASU reconstructs the native render +better than bilinear. + +KEPT NEGATIVES (loud): classical spatial upscaling is BELOW learned (DLSS/XeSS) -- it reconstructs, it cannot invent +detail absent from the low-res input. EASU's artifacts get MULTIPLIED by the RCAS sharpen: on a SMOOTH render the +sharpen OVERSHOOTS (fsr edge energy 0.0119 > native 0.0107, PSNR drops), so sharpness is a KNOB, not a free win -- +which is why the PSNR win is measured on EASU alone. This EASU is an honest Lanczos-with-anti-ringing in FSR1's class, +not a byte-for-byte port of its 12-tap gradient-reversal kernel. + +Inverse-rendering progress: auto-material (IR1/IR7/ST1/IR5) + scene-matching (IR3->IR4) + IR14 (channels) + IR10 (SVGF, +verified) + IR12 (FSR upscale) done. Remaining: IR13 (checkerboard render -- reconstruction = image masked-recovery, +the "shade half" mode is the new bit), ST2/ST3 (example-based texture/super-res), IR11 (3D splat-bundle archive), +IR2/IR8/IR9 (opt-in), IR6 (a boundary, not a build). + +## Inverse-rendering IR13: checkerboard/sparse rendering -- shade half, recover the rest (+8 tests) + +holographic_checkerboard.py. Shade only ~50% of pixels (a 2x2 checkerboard, parity flips per frame) and RECONSTRUCT +the rest -- roughly halving shading cost for near-full-resolution. The holographic reading (Ozcan's seat): the +unshaded pixels are "damage", reconstruction is recovery from a partial/masked measurement -- the archive's job in the +pixel domain. Probe-first correction: holographic_image.reconstruct(mask=) is PLATE-specific (masks WHT/DCT +measurements, not spatial pixels), so the checkerboard needs a SPATIAL fill -- and the 2x2 gem is that every unshaded +pixel's four cross-neighbours are all shaded, so recovery is a clean cross-neighbour average (no iteration, no learned +prior). checkerboard_mask, reconstruct_checkerboard (cross-neighbour fill, shaded pixels kept exact), render_checkerboard +(traces ONLY the masked rays via the shared _shade_rays Lambert+sky -> real cost saving, then reconstructs). Faculty: +render_checkerboard. + +MEASURED (selftest + 7 pytests + integration): mask shades 50%, parity flips it (m1 == ~m0); reconstructing from the +checkerboard matches a full shade at 36.1 dB (>30), beating no-fill (15.5 dB) AND a matched-cost row-halved render +(33.4 dB) -- the documented CBR advantage: spreading 50% of samples in 2D beats collapsing them in 1D; shaded pixels +kept exact; render_checkerboard traces only the masked half and reconstructs to 36.1 dB; deterministic. Integration: +through the mind, ~half the pixels shaded, reconstruction ~ full quality, shaded pixels exact. + +KEPT NEGATIVES (loud): reconstruction is a TRADE (better accuracy per render cost), NOT free -- it costs more than a +plain lower-res render but reconstructs more accurately at matched cost. Under MOTION it can shimmer unless the +temporal reprojection rejects disoccluded pixels by depth/motion (out of scope for this single-frame v1). Colour/ +visibility exist only at render-target resolution -> a reconstruction, not true supersampling. + +Inverse-rendering progress: auto-material (IR1/IR7/ST1/IR5) + scene-matching (IR3->IR4) + IR14 (channels) + IR10 (SVGF, +verified) + IR12 (FSR) + IR13 (checkerboard) done. Remaining: ST2/ST3 (example-based texture/super-res), IR11 (3D +splat-bundle archive), IR2/IR8/IR9 (opt-in light-aware/photometric/intrinsic), IR6 (a boundary, not a build). + +## Inverse-rendering IR11: 3D object archive -- recall the whole from a partial view (+8 tests) + +The honest answer to the back-of-the-object boundary. holographic_objectarchive.py. Store a library of COMPLETE 3D +objects; from only a partial FRONT view, recall the nearest stored complete object by a view fingerprint and return +the WHOLE thing (incl. the unobserved back), or ABSTAIN when nothing matches. Retrieval, not hallucination -- the +engine's OLDEST move (cleanup/consolidation/resonator/analog), pointed at geometry: a scene is a bundle of splats as +a memory is a bundle of role-bound vectors. object_fingerprint (coarse front-silhouette occupancy+depth grid, unit- +norm -> HoloForest-indexable), front_points (camera-facing half), ObjectArchive (add/build/complete_from_front with a +similarity-floor abstain), _chamfer. Faculty: complete_object. + +Probe-first note: the 2D splat/photo3d ship, but their WHT-plate masked recovery is for dense image plates, not point +sets; the honest 3D completion is analog-recall retrieval (recall the stored whole whose front matches), which IS the +"recover the whole from a partial measurement" move for geometry. + +MEASURED (selftest + 7 pytests + integration): from a NEW sphere instance's front half, recalled the whole sphere BY +SHAPE (similarity 0.99, not memorized points -- a different instance), recovering the unobserved back to Chamfer 0.078 +vs the front-only baseline's 0.685 (9x closer); box/cylinder recall their own shape; abstained on a cone not in the +library (similarity 0.73 < floor 0.85). Fingerprint unit-norm + stable with ~500 points (same-shape 0.99 vs +different-shape 0.73, margin 0.26); deterministic. Integration: through the mind, recall + back-recovery + abstain. + +KEPT NEGATIVES (loud): COVERAGE-LIMITED -- completes only STORED objects; outside the library it ABSTAINS (honest +output on an unseen shape is "front only", never an invented back); the win scales with library coverage -- retrieval, +not hallucination. REGISTRATION is coarse (fingerprint gives the match + abstain gate; the IR4 loop refines pose); a +wrong match must surface as LOW similarity and abstain, never a confident-wrong completion. Capacity is a dial not a +wall (disjoint slots / tiling / importance-order escapes); inherits the splat archive's lossy/isotropic trade. + +Inverse-rendering: auto-material (IR1/IR7/ST1/IR5) + scene-matching (IR3->IR4) + IR14 (channels) + render-speed +(IR10 SVGF verified, IR12 FSR, IR13 checkerboard) + IR11 (3D object archive) done. Remaining: ST2/ST3 (example-based +texture/super-res -- patch search = HoloForest recall), IR2/IR8/IR9 (opt-in light-aware/photometric/intrinsic), IR6 +(a boundary, not a build). + +## Inverse-rendering ST2: example-based texture synthesis (Image Quilting) -- patch search = HoloForest recall (+7 tests) + +holographic_texturesynth.py. Grow a larger (optionally seamless) texture from a small sample -- material synthesis, +feeding IR1 auto-bump with tileable maps. NO learned weights. Image Quilting (Efros & Freeman 2001): lay overlapping +patches copied from the sample, choose each so its overlap with the placed patches MATCHES, then stitch along the +least-error MIN-CUT seam so the joins vanish. Two pieces map onto shipped primitives: the patch search 'find a sample +patch whose border matches this context' IS HoloForest.recall_k (the same 'find patches like this one' NLM uses -- used +here to narrow candidates sublinearly, then exact overlap-SSD picks the winner), and the min-cut is a small DP. +synthesize_texture (seam='mincut'|'hard'), find_similar_patches (the native patch search), min-cut vertical/horizontal. +Faculty: synthesize_texture. + +MEASURED (selftest + 6 pytests + integration): quilted a 48x48 sample into 96x96 whose mean/std match (0.46/0.21 vs +0.46/0.22); the min-cut seams LESS than a hard cut on the SAME patches (0.1131 vs 0.1137 -- small margin because it +only reroutes the seams, but consistent, isolating the min-cut benefit); the native HoloForest patch search finds +similar patches (top cosine 1.00); grayscale in -> grayscale out; deterministic. Integration: ST2 -> IR1 -- synthesize +a larger texture, then auto-bump it into a valid normal map (the two threads compose). + +KEPT NEGATIVES (loud): patch-COPYING -- can repeat or seam (min-cut mitigates the seam; variety comes from picking +among the near-best, not just the best). BELOW neural for arbitrary artistic styles (Image Analogies "gave poor +synthesis" next to Gatys); best for TEXTURE/colour/material on a roughly-stationary sample, NOT a structured scene or +free-form painterly restyle. The seam-energy metric measures TOTAL gradient (a varied quilt naturally has more than a +single repeated patch) -- so the honest min-cut test is min-cut vs hard-cut on the SAME patch choices, not vs a naive +single-patch tiling. + +Inverse-rendering: auto-material (IR1/IR7/ST1/IR5) + scene-matching (IR3->IR4) + IR14 (channels) + render-speed (IR10/ +IR12/IR13) + IR11 (object archive) + ST2 (texture synth) done. Remaining: ST3 (example-based super-res -- an image +analogy / self-similar upsample, patch search again), IR2/IR8/IR9 (opt-in light-aware/photometric/intrinsic), IR6 (a +boundary, not a build). + +## Inverse-rendering ST3: guided (joint-bilateral) super-resolution -- render small, upscale by the G-buffer (+5 tests) + +The render-speedup, reusing the shipped SVGF bilateral. holographic_superres.py. A cheap render shades COLOUR at low +resolution, but the GEOMETRY (normal/depth G-buffer, which IR14 render_channels exposes) is available at FULL +resolution because tracing it is cheap -- so coarsely upscale the low-res colour (easu), then edge-aware-filter it +GUIDED by the full-res G-buffer via holographic_svgf.atrous_bilateral (with sigma_color set HIGH so the blurry colour +doesn't drive the edges -- the GUIDE does). Colour edges snap to the geometry. guided_upsample. Faculty: guided_upsample. + +MEASURED (selftest + 4 pytests + integration): guided upsample of a 32x32 render, steered by the 64x64 G-buffer, +reaches 38.28 dB to native vs a plain easu upscale's 30.46 dB (+7.8 dB) -- the geometry pulls the blurry colour edges +back into place; normal-only guide works (albedo/depth default to it); in range; deterministic. Integration: through +the mind, render channels (IR14) -> guided upsample (ST3) beats plain upscale. + +SCOPE DECISION (honest): the audit named two ST3 routes -- guided upsampling (built, the strong win) and self-similar/ +example SR. A first self-similar impl was SLOW (rebuilt a HoloForest per patch position, 61s) and its patch search was +DECORATIVE (it borrowed each patch's own residual, not a matched patch's) -- so it was REMOVED rather than shipped +muddled. The guide-free route composes ST2's shipped find_similar_patches (the same HoloForest patch search) rather +than duplicating it; guided upsampling is this module's deliverable. + +KEPT NEGATIVES (loud): classical upsampling INVENTS PLAUSIBLE, NOT TRUE, detail and tops out BELOW learned SR -- it +snaps/borrows structure, it does not recover information never sampled. Guided upsampling needs a CLEAN full-res guide +(a noisy guide leaks into the colour); the G-buffer supplies a clean one. + +INVERSE-RENDERING BACKLOG -- genuinely-new items COMPLETE: auto-material (IR1/IR7/ST1/IR5), scene-matching (IR3->IR4), +render channels (IR14), render-speed (IR10 SVGF verified, IR12 FSR, IR13 checkerboard), 3D object archive (IR11), +style/texture (ST1/ST2/ST3). Remaining are the honestly-scoped OPT-IN items (IR2 light-aware height, IR8 photometric +stereo, IR9 intrinsic/Retinex -- all lower priority, need multi-light or are ill-posed) and IR6 (a boundary, not a +build). Good point to do the end-of-backlog full tour run. + +## Query layer Tier FIX (F1-F3): three measured bugs corrected (+7 tests) + +Query backlog Tier FIX -- the bugs an evaluator hits first. All three REPRODUCED before fixing (honest measurement), +then fixed in holographic_query.py, then pinned. No new module/faculty -- internal engine correctness. + +- F1 unknown-column error: SELECT/WHERE/GROUP/aggregate/ORDER on a column the table never declared returned a + confident null ({'nope': None, '_confidence': 1.0}); now Query.run validates every referenced column against + table.roles and raises QueryError "column does not exist". Careful cases kept working: a DECLARED-but-absent column + (sparse row) still reads as None (not an error), and ORDER BY an AGGREGATE LABEL (e.g. COUNT(*)) is allowed (the + first fix was too strict and broke test_capability_registry_is_queryable -- caught by the existing suite, fixed). +- F2 clean multi-predicate rejection: a WHERE with AND/OR let the single-predicate regex swallow "5 AND ..." into the + value and leaked a TypeError on the compare; now parse_sql detects AND/OR OUTSIDE quotes and raises a clear + "only one WHERE predicate is supported" (a legit value like 'black and white' is NOT misread -- quotes stripped + before the check). Superseded later by B3 (real predicate tree). +- F3 LIMIT 0: run_sql did `if plan["limit"]:` so 0 (falsy) dropped the limit and returned all rows; now + `if plan["limit"] is not None:` -- LIMIT 0 returns no rows. (Query.run's own idx[:0] was already correct.) + +MEASURED: all three reproduced then fixed; 7 pinning tests; existing query/query_db/graphql suites green (31 passed). +KEPT NEGATIVE: F2 is a clean REJECTION, not multi-predicate support -- that is B3. This is the FIRST item of the query +backlog (FIX -> PROMOTE -> BUILD -> programs -> workspaces). Running BOTH the query and fluids backlogs before the +next full tour (per the current instruction). + +## Query layer PROMOTE P1-P3: shipped faculties as query verbs (+8 tests) + +Query backlog PROMOTE tier -- the differentiated half is already built as faculties; the query layer just hadn't +reached out and wired them in (query.py imported only the kernel -- confirmed). Three verbs added to +holographic_query.py, operating on a Table (and proven through the Database front door): + +- P1 similar_to(table, target_row, k): whole-row "more like this" -- cosine of the WHOLE record vector (every column + at once, not per-column like pgvector), ranked with a per-row confidence. MEASURED: from a table of 3 cats/3 birds/ + 1 snake, similar_to a cat returns the 3 cats at confidence 1.00. +- P2 cluster(table, into, seed): semantic GROUP BY -- cosine k-means (reuses organizer._cosine_kmeans) with per- + cluster COHERENCE (mean cosine to centroid) so a tight group is distinguishable from a loose one; empty clusters + dropped. MEASURED: the 3 identical meows form a coherence-1.00 cluster; the snake joins the looser one. +- P3 anomalies(table, seed): calibrated "how weird" -- per row, RecallNull.pvalue that its NEAREST OTHER row is only + a noise-level match; high => no real neighbour => anomalous, and it can ABSTAIN (all scores tiny) instead of always + naming N outliers. MEASURED: the lone snake scores 0.78 (nn_sim 0.01 = noise), duplicated rows score 0.000; a + table where every row has a duplicate yields max score < 0.2 (nothing anomalous). + +Reuses cosine (P1), organizer._cosine_kmeans (P2), honesty.RecallNull (P3). No new module -- verbs on the query +layer. 8 tests incl. a Database-front-door integration. KEPT NEGATIVE: P2 clustering is unsupervised (a suggestion, +not a decree; `into` is a hint). Query backlog progress: FIX (F1-F3) done; PROMOTE P1-P3 done. Next PROMOTE: the +history enabling wire + P7 (time-travel) / P9 (diff), then P4-P6. + +## Query layer PROMOTE P4-P6: fuzzy dedup, match explanation, recommendation (+7 tests) + +Rest of the "a row is a vector" promote family in holographic_query.py: +- P4 near_duplicates(table, threshold): fuzzy dedup/entity resolution -- union-find over the >=threshold cosine pairs + of whole record vectors, returns duplicate GROUPS (size>1) with mean intra-similarity, tightest first. MEASURED: + the meow-trio and tweet-pair found (sim 1.00), the lone hiss excluded; all-distinct -> []. PROPOSES candidates, a + human/threshold confirms (kept negative). +- P5 explain_match(table, a, b): WHY similar -- respects the EXACT/FUZZY FORK. A categorical (string) column is + compared on the vector side (unbind the role from both records, cosine the fillers -- the resonator's decompose as + interpretability); a NUMERIC column is compared on the exact stored side (numerics deliberately never enter the + record vector). PROBE-FIRST CATCH: first version unbound EVERY column and gave nonsense for numerics (legs=4 vs 4 + read as noise, 0.0) -- fixed to honor the fork. MEASURED: cat vs 4-legged-tweeter -> drives=[legs] against=[sound] + (shares the numeric, differs on the categorical), distinct from cat-vs-bird (shares nothing). +- P6 recommend(table, example_rows, k, exclude): "more like these" -- bundle the examples into one taste vector, + recall nearest by cosine, exclude the examples by default. MEASURED: recommending from a cat excludes the cats. + +Reuses cosine (P4/P6), unbind (P5), bundle (P6). Query backlog: FIX done; PROMOTE P1-P6 (the row-is-a-vector half) +done. Next: the history enabling wire + P7-P12 (time-travel/diff/branch/audit) which share it. + +## Query layer PROMOTE P7-P12: the versioned table -- time-travel/diff/branch/audit (+9 tests) + +holographic_query_history.py -- the enabling wire the backlog calls for (do once, P7-P12 are thin verbs). VersionedTable +wraps a UserTable; commit() snapshots BOTH halves of the fork -- record VECTORS into a VersionedStore (shipped, delta- +compressed, exactly recoverable) and the exact stored ROW-DICTS per version. Reuses holographic_history.VersionedStore +(commit/checkout/rollback), holographic_deltachain.merkle_root (proof), hashlib (row hashes). No new versioning engine. + +- P7 select_as_of(sql, version): checkout-then-query; SQL:2011 temporal tables are painful, this is one call. +- P8 history_of(key_value, key): one row's timeline (blame). +- P9 diff(va, vb, key): with a key -> exact added/removed/CHANGED; keyless -> multiset content diff (added/removed). +- P10 revert(version): restore live table to a past version, RECORDED as a new commit (history never erased). +- P11 branch(): fork an independent timeline sharing the past; diff against main, keep or discard. MERGE deliberately + not built (needs an explicit conflict policy -- kept negative). +- P12 prove(version)/find_tampering(claimed, version): Merkle root over hashlib row hashes; locate the altered row in + O(n); a single change flips the root. + +MEASURED (selftest + 9 pytests): committed versions time-travel (v0=2 rows, v1=3); diff-by-key finds carol added + +bob's balance changed 50->75; blame tracks bob 50->75; a tampered row located at index 0 and flips the root; a branch +diverges (adds dave) without touching main; revert restores v0 as a new version; deterministic. + +KEPT NEGATIVES: rows matched by KEY when given one (positions shift on add/remove); keyless diff is added/removed only +(a change = one removed + one added). Query backlog: FIX + PROMOTE P1-P12 done (the whole differentiated PROMOTE tier +except P13). Next: P13 (verify sparse/no-migration works), then BUILD tier (JOIN/UPDATE/DELETE/multi-WHERE/PK-index). + +## Query PROMOTE P13: add-a-column-no-migration + sparse rows (+3 tests) + +Confirmed + made real. UserTable.add_column(name) appends a role (allocates its codebook vector) with NO re-encoding +of existing records -- old rows stay sparse (never bound it -> read None), new rows may set it, and it becomes +queryable. MEASURED: existing records bit-identical after add_column; old rows read None, new row's value queryable +(WHERE age > 25 exact on stored). This is what "a record only holds the roles you bind" buys -- ALTER TABLE ADD COLUMN +is free where SQL locks/rewrites. Completes the query PROMOTE tier (P1-P13). Next: BUILD tier (JOIN, UPDATE/DELETE, +multi-predicate WHERE, PK index). + +## Query BUILD B3: multi-predicate WHERE (AND/OR/parentheses) via a predicate tree (+9 tests net) + +holographic_query.py. The parser now builds a WHERE predicate TREE instead of a single (col,op,val). A small readable +recursive-descent parser (_tokenize_where + parse_where/_parse_or/_parse_and/_parse_factor) with the usual precedence +(OR lowest, AND higher, parens highest); a single predicate is just a leaf, so the old behaviour is a special case. +Query.where_tree(tree) + _leaf_mask (one predicate -> a row keep-set; exact on stored props, ~ fuzzy) + _eval_where +(intersect/union the per-leaf masks). Operators widened to = > < >= <= != ~. F1 validation now walks _where_columns; +fuzzy-default ranking via _tree_has_fuzzy. + +MEASURED: AND (cat,ant), OR (cat,crab,ant), parens override precedence, AND binds tighter than OR, >=/<=/!= work, +single predicate still works, malformed/unbalanced raises, unknown column in a multi-predicate errors. SUPERSEDES F2 +(clean rejection): AND/OR now actually work -- updated the F2 test from "raises" to "works", and test_sql_parser_subset +to expect the tree leaf ('pred','c','~','x'). Existing query/query_db/promote/history/graphql suites green (55 passed). + +KEPT NEGATIVE: readable recursive-descent, not a full SQL grammar (no BETWEEN/IN yet -- those are B9). Query backlog: +FIX + PROMOTE (P1-P13) + BUILD B3 done. Next BUILD: B1 (JOIN), B2 (UPDATE/DELETE), B4 (PK index). + +## Query BUILD B1: JOIN -- exact hash-join + fuzzy/semantic join (+7 tests) + +holographic_query.py. join(left, right, on, how) -- exact HASH-JOIN: build a dict on the RIGHT key once (the hash +side), probe per LEFT row -> O(n+m) not O(n*m). inner + left (null-fills the right on a miss). `on` is a shared key +name or a (left_key, right_key) pair. Shared non-key column names disambiguated with suffixes; the join key appears +once. _joined_row does the merge. MEASURED: users x orders -> alice x2/bob x1 (inner), carol null-filled (left), +collisions suffixed v_l/v_r, different key names work, bad key errors. + +fuzzy_join(left, right, on, threshold, key_encoder) -- the SEMANTIC join SQL can't do. PROBE-FIRST CATCH: first version +unbound each table's key filler, but the two tables have INDEPENDENT value vocabularies (different seeds) so identical +string keys were orthogonal -> matched nothing. Fixed to encode both keys in a SHARED space. Default categorical +encoder -> identical keys match (reduces to exact, the honest baseline); scalar_key_encoder(lo,hi) -> NUMERIC keys +that are merely CLOSE match (a proximity join). MEASURED: categorical identical keys match at 1.00; numeric ts=10 +matches ts=11 (conf 0.91) but NOT ts=50/80 -- a genuine timestamp-window join. KEPT NEGATIVE: fuzzy join is +APPROXIMATE and O(n*m); use the exact hash-join when keys are exact. + +Query backlog BUILD: B3 (multi-WHERE) + B1 (JOIN) done. Next: B2 (UPDATE/DELETE append+tombstone+compaction), +B4 (PK/hash index for the O(n) scan). + +## Query BUILD B2: UPDATE/DELETE via append-only tombstone + compaction (+6 tests) + +holographic_query.py. A record is a bundle (a sum) -- you can't cleanly subtract one -- so writes are LSM/event- +sourced. delete(table, where) tombstones matching rows (_deleted flag in the stored dict, NOT a role -> invisible to +SELECT/validation); update(table, where, changes) tombstones the old version and APPENDS a new one; compact(table) +replays live rows into a fresh table (reclaims the retired vectors' crosstalk). Query.run now skips tombstoned rows +in every scan (idx excludes _deleted). _matching_indices(table, where) reuses parse_where + Query._eval_where (so +delete/update accept the same AND/OR/parens WHERE as SELECT). + +MEASURED: delete crab (legs>6) -> scans skip it; update bird legs 2->3 -> new value queryable, old (legs=2) gone; +raw rows grow (5: 4 orig + 1 new bird, 2 tombstoned) then compact -> 3 live rows; can delete an updated version. +Existing query/query_db suites green (no regression from the _deleted skip). + +KEPT NEGATIVE: tombstones GROW the table until compact(); the retired rows' vectors carry crosstalk until GC (fine +for exact reads, mildly degrades fuzzy recall between compactions). Query BUILD: B1/B2/B3 done. Next: B4 (PK/hash +index for the measured O(n) scan). + +## Query BUILD B4: primary-key hash index -- O(1) pk lookup + a latent perf-bug fix (+6 tests) + +holographic_query.py. UserTable.set_primary_key(col) builds a dict {value: [row indices]}, maintained on insert; +pk_lookup(value) returns live indices (skips tombstones) O(1). Query.run has a fast path: a single `pk = value` +predicate reads the index directly instead of the O(n) scan (the measured bug). Stays in sync through insert/update/ +delete (update appends a new version -> the index points at the live one; delete tombstones -> lookup skips it); the +replay model rebuilds it on load so it never drifts. + +PROBE-FIRST / MEASURED-HONESTLY CATCH (a real find): the first cut only got ~2x because run still scanned. Diagnosing +by component (pk_lookup flat, parse flat, q.run O(n)) exposed a LATENT PERFORMANCE BUG affecting EVERY query: the row +projection used table.rows[i].get(c, project(...)) and Python evaluates that project(...) DEFAULT EAGERLY even when the +stored value exists -- and project() runs a cleanup over the value codebook, which grows O(distinct values). Fixed to +only decode from the vector when the column is truly absent. Result: indexed lookup is now genuinely FLAT (0.009ms at +2k AND 10k rows) vs the O(n) scan (0.38ms -> 1.81ms) -> 41x at 2k, 194x at 10k; and the fix sped up ALL queries (the +scan itself roughly halved). Made sims lazy too (no [1.0]*n per query). + +All 80 query tests green (behavior preserved). This COMPLETES the query BUILD B-1 tier (B1 JOIN, B2 UPDATE/DELETE, +B3 multi-WHERE, B4 PK index) -- the day-one relational gaps. KEPT NEGATIVE: the index is exact-key only; large VECTOR +lookups would wire the shipped sublinear indexes (pivot/HoloForest) -- deferred. Next: B5 constraints / B6 transactions, +or the PR (VSA-programs-as-DB-objects) / WS (workspaces) waves, then the fluids backlog. + +## Query BUILD B5+B6: constraints + single-writer transactions -- make writes safe (+13 tests) + +holographic_query.py -- the "make writes safe" tier. + +B5 constraints (enforced on insert, BEFORE any state change -> a violating row is refused and the table left +unchanged): UserTable.not_null(*cols), unique(*cols), foreign_key(col, ref_table, ref_col), check(predicate, name); +set_primary_key now also implies NOT NULL + UNIQUE. _enforce_constraints raises ConstraintError (a QueryError +subclass). FK allows a None value (resolve-or-null soft opt-out); a non-None value must resolve to a live row in the +referenced table. MEASURED: PK dup/null, non-pk UNIQUE, FK dangling, CHECK all refused with clear messages; valid +inserts unaffected; a table with no constraints declared enforces nothing. + +B6 transactions (single-writer atomicity): transaction(*tables) context manager snapshots each table (records + rows ++ pk index) on entry; on ANY exception it rolls every table back to that snapshot; on clean exit it commits. Rollback +(exception) aborts cleanly and is swallowed. MEASURED: a constraint violation mid-batch undoes the whole batch; +explicit Rollback aborts with no escape; clean exit commits; the PK index is restored on rollback (a later insert of +the rolled-back key is allowed); a multi-table txn rolls back ALL tables. Broad query regression green (56 passed). + +KEPT NEGATIVES: B6 is SINGLE-WRITER atomicity + rollback -- isolation between CONCURRENT writers is deferred (B8), +which covers many workloads honestly without over-promising serialisable isolation. UNIQUE/FK checks scan live rows +(O(n)); same order as the vstack insert, acceptable, and the PK path stays O(1) via its index. Query BUILD: B1-B6 done +(day-one gaps + safe writes). Next: B7 durability / B8-B10, or the PR (programs) & WS (workspaces) waves, then fluids. + +## Query BUILD B9: SQL-surface fill-ins -- DISTINCT / OFFSET / HAVING / UNION (+9 tests) + +holographic_query.py. parse_sql extended: SELECT [DISTINCT] ... [HAVING agg op v] ... [LIMIT n] [OFFSET m]. Query got +_distinct/_offset/_having + setters; a module _compare() comparator. run: OFFSET+LIMIT trim the index directly without +DISTINCT (cheap), but with DISTINCT they apply to the DEDUPED rows (correct ordering). _run_aggregate applies HAVING +(filter groups by an aggregate result) + OFFSET. run_sql handles UNION / UNION ALL by splitting on the keyword, +running each SELECT via _run_single_select, and combining (any plain UNION dedupes the whole result; ALL keeps dupes). + +MEASURED: DISTINCT habitat -> [land, water]; multi-col DISTINCT -> 5 distinct (legs,habitat) pairs; ORDER+LIMIT+OFFSET +skips then takes; OFFSET past the end -> []; GROUP BY ... HAVING COUNT(*)>=2 keeps both 3-groups, HAVING >1 excludes a +group of 1; UNION dedupes (crab + land trio), UNION ALL keeps 6 with overlaps; DISTINCT+LIMIT limits the deduped rows. + +PROBE/DEBUG CATCH: an earlier ordering-fix str_replace had dropped run's projection+return block (run returned None); +restored it. Full query regression green (93 + b9). HAVING is a single predicate (readable; multi-predicate HAVING is +a noted limit). Query BUILD now B1-B6 + B9. Remaining: B7 (durability), B8 (concurrency), B10 (graph), then PR (VSA +programs as DB objects) + WS (workspaces), then the fluids backlog. + +## Query PR1-PR6: VSA programs as installable, runnable database objects (+10 tests) + +holographic_query_programs.py -- pg_proc-style stored procedures where the "procedure" is a hypervector the VSA +machine executes. Mostly PROMOTE (listing/explaining/running ship); only install + the execute bridge are new. +ProgramCatalog(dim, seed, faculties): install(name, program, doc, inputs, outputs, allowed_handlers, tier) assembles +the instruction list to a program vector (machine.assemble) and stores a bag-of-words DESCRIPTOR (shared word atoms) ++ the sandbox whitelist; uninstall refuses system programs. Verbs: catalog_table(mind) [PR1, a query Table of user +programs + the mind's faculties via capability_registry, with a tier column], find(query_text) [PR4, bag-of-words +cosine over docs -- semantic recall a SQL catalog can't do], explain(name) [PR3, handler-less dry run via +explain_program], execute(name, accumulator, handlers, max_steps) [PR2/PR6, runs the program IN THE VECTOR DOMAIN +over an accumulator; SANDBOX = only whitelisted handlers callable, APPLY to anything else is a safe no-op; step limit]. +encode_rows_accumulator bundles rows into the accumulator. + +MEASURED (selftest + 10 pytests): programs catalogued + tier-tagged; find('group a signal over time') -> cluster_series +(not tagger), find('label this record') -> tagger; EXPLAIN names ['tag'] in 1 step; execute runs the tagger (output +cosine 0.036 to input, unbinds back to input at >0.9 with a unitary tag); the sandbox refuses a non-whitelisted 'wipe' +handler (accumulator unchanged, cosine >0.99); step limit bounds the trace; user programs uninstall, system refused; +integration: the mind's catalog lists >50 system faculties. PROBE CATCH: first execute test used a non-unitary tag +atom (unbind recovered only 0.70) -> switched to machine._atom(unitary=True) for a near-exact round-trip. + +KEPT NEGATIVES: results are on the FUZZY side (a program on decoded vectors carries readback error -> a confidence, +can abstain); find-by-meaning is bag-of-words (keyword-semantic), not a learned embedding; system programs read-only. +Query backlog: FIX + PROMOTE (P1-P13) + BUILD (B1-B6,B9) + PR (PR1-PR6) done. Remaining: WS (workspaces), B7/B8/B10, +then the fluids backlog. + +## Query WS1-WS6: workspaces -- durable DB coexists with transient sessions (+9 tests) + +Three tiers so a persistent database and per-session 3D/sim scratch never wipe each other. +WS1/WS2 in holographic_query.py (Database): each namespace carries a TIER (system read-only / persistent durable / +workspace transient); add_namespace infers tier from writable (backward compatible), create_namespace(tier), +drop_namespace (system protected), tier_of; to_state(tiers=[...]) saves only chosen tiers (persistent for the durable +DB, workspace for one session). +WS3-WS6 in holographic_workspace.py: Workspace (a ws: namespace + scene/sim/render handles) and WorkspaceManager +over a Database. new/switch/clear workspaces (clear drops only that namespace); reset_to_default drops ALL workspaces +but KEEPS the persistent tier (the 'new session' button that does not destroy user data); export/import one workspace +by replay (deterministic); combine_workspaces unions two workspaces with an EXPLICIT collision policy +(error/suffix/left/right -- a merge needs a decision, not a guess). + +MEASURED (selftest + 9 pytests): persistent 'notes' survives reset_to_default and workspace clears; clearing +workspace A leaves sibling B and the persistent DB intact (isolation); export->clear->import round-trips a workspace's +rows; to_state tier-scoping returns exactly the requested tier's namespaces; combine refuses a clash by default, +'suffix' keeps both (t_a/t_b), 'left' picks the left winner; system tier cannot be dropped or created into. No +regression in query_db/query suites. + +KEPT NEGATIVE: reset keeps persistent + drops workspaces, but the SYSTEM tier is the mind's to re-publish (not +fabricated here); combine defaults to 'error' so a merge is a deliberate choice. + +QUERY BACKLOG now: FIX + PROMOTE (P1-P13) + BUILD (B1-B6,B9) + PR (programs) + WS (workspaces) all done. Remaining +query: B7 durability, B8 concurrency, B10 graph traversal (has the graph-memory recall-collapse caveat). Then the +FLUIDS/MATTER/SCALE backlog. Full tour still parked until both backlogs are complete (user instruction this span). + +## Query BUILD B7+B8+B10: durability, concurrency, graph traversal -- QUERY BACKLOG COMPLETE (+17 tests) + +B7 durability (holographic_query_durable.py): snapshot + append-only redo journal on the replay spine. save_snapshot +(to_state -> JSON, atomic write-then-rename + fsync, persistent tier by default), load_snapshot (from_state), Journal +(log_insert/update/delete, each flushed+fsynced; entries/replay/truncate), recover(snapshot, journal) = load + replay. +Also made from_state TIER-AWARE (restores workspace vs persistent per the saved tier). MEASURED: recover after a +simulated crash yields snapshot rows + journalled insert + journalled update (apple qty 9, pear, plum); delete +journalled + recovered; re-snapshot folds the log, truncate safe; no stray .tmp. KEPT NEGATIVE: a write must be +journalled+flushed BEFORE the crash to survive (WAL durability bound); in-process, JSON not pickle. + +B8 concurrency (holographic_query_concurrency.py): single-writer / multi-reader. write_lock(table, blocking) = a +threading.Lock per table (id-keyed), mutually exclusive (second non-blocking acquire refused while held). +snapshot_reader(table) = a cheap point-in-time COPY (rows+records+pk index) that run_sql queries unchanged, isolated +from later writes. MEASURED: lock exclusive + frees on release; snapshot froze at 1 row while live grew to 2; two +threaded writers under the lock landed all 10 inserts, none lost (test miscount fixed 12->11). KEPT NEGATIVE: +SINGLE-writer serialised (not multi-writer serialisable); snapshot is a copy (memory, stable read); in-process lock. + +B10 graph traversal (holographic_query_graph.py): descendants/ancestors/shortest_path/reachable over EXACT adjacency +built from an edge table (build_adjacency, BFS). DELIBERATE: exact adjacency NOT holographic graph memory, because the +holographic graph recall COLLAPSES at scale (kept negative) -- the store owns the edge DATA, traversal is a correct +plain walk. Skips tombstoned edges. MEASURED: org-chart descendants/ancestors, shortest ceo->eng3=[ceo,vp2,eng3], +cross-branch None, directional reachability, deleting ceo->vp2 unhooks eng3, cycle terminates (excludes start). + +QUERY BACKLOG COMPLETE: FIX (F1-F3) + PROMOTE (P1-P13) + BUILD (B1-B10) + PR (programs) + WS (workspaces). Next: the +FLUIDS/MATTER/SCALE backlog (mind faculties + tour blocks), THEN the full suite + full tour (user's deferred +end-of-both-backlogs checkpoint). + +## Fluids/matter item 1: SMOKE PRESETS -- six named looks over the wired solver (+9 tests) + +FLUIDS/MATTER/SCALE BACKLOG STARTED. Probe-first confirmed the substrate is live: advect_field/diffuse_field/ +buoyancy_force/fluid_step/smoke_step/scatter_to_field on UnifiedMind; fluid/fields/automaton/emitter/volint/iterate/ +compile/fuse/prt/sdfbake/surface/procgen/scene_doc/distribute modules all present. So smoke needs NO solver -- only +looks. holographic_smokepresets.py: SMOKE_PRESETS (rising/wispy/billow/heavy/still_room/stratified) = dial bundles +(buoyancy/confinement/viscosity/gravity/ambient + source kind) over fields.smoke_step; simulate() runs it (brief +source injection then evolve); plume_center_of_mass, render (volint is the full volumetric path). Faculty: smoke_preset +/ smoke_preset_names on UnifiedMind (delegates). + +PROBE/MEASURE CATCH: buoyancy convention is +row = up (a hot central puff COM 0.5->0.57, a heavy one ->0.46); base +sources with continuous injection pinned COM at the source, hiding dynamics -> switched to brief injection + a +controlled central-puff physics check (_buoyant_vs_heavy) so the dial is validated independent of source placement. +MEASURED: six presets give distinct COMs (rising 0.10, wispy 0.08, billow 0.11, heavy 0.23, still_room 0.50, +stratified 0.06 -- 4+ distinct); still puff hangs mid; buoyancy rises above centre, gravity sinks below. Integration +test (test_integration.py): smoke_preset -> sample_field reads the plume density > an empty corner (matter proved +through the sampler). Tour block added (verified isolation; tour parses/compiles; NOT run per user's both-backlogs- +first instruction). + +KEPT NEGATIVE: 2-D looks on a modest grid; any solver limitation (coarse grid smears fine curl) is INHERITED not +introduced; presets add zero physics, only dials. Next fluids items: Mixture + matter_step (dye/milk), then drift +(salt fingering), double_well (oil&water), ScatterLayer/ScaleNode, then the compile/bake performance half. + +## Fluids/matter item 2: Mixture + matter_step -- the multi-channel matter model (+8 tests) + +holographic_mixture.py. The thesis made concrete: smoke/dye-milk/salt-fingering/oil-water are ONE advected-field model +with three dials, not four solvers. Mixture(shape, solvent_density, buoyancy, tension): channels (name->concentration +field, a volume fraction) + comp (name->Component(density, diffusivity)) + temperature. density() = solvent + sum +phi*(comp.density-solvent) = the fraction-weighted BUNDLE. renormalise() keeps a valid partition (clamp >=0, scale +back where sum>1). matter_step(mix, vx, vy, dt, drift_strength): blend density -> buoyancy_force -> ONE fluid_step -> +per-channel advect + diffuse (+ optional _double_well when tension>0, + optional _drift) -> renormalise. DELEGATES to +wired advect/diffuse/buoyancy_force/fluid_step -- NO second solver. drift (item 3) + double_well (item 4) wired as +OPTIONAL hooks in the loop so they slot in with no rewrite. Faculties: make_mixture, matter_step on UnifiedMind. + +MEASURED: two dye channels advect + diffuse on one shared flow (red spread 5.66->5.77), mass conserved within 15% +(diffusion conserves on the torus), density blend reads heavy-dye 1.19 vs light-dye 0.81 (solvent 1.0); per-channel +diffusivity differs (fast spreads more than slow -- the salt-fingering precondition); renormalise caps overfilled +cells; deterministic. Integration test: make_mixture -> matter_step -> sample_field reads heavy region denser than +light (matter through the same sampler as smoke). Tour block added (verified isolation; tour parses/compiles; not run). + +KEPT NEGATIVE: miscible mixing is native/cheap; the SHARP immiscible interface is item 4's diffuse-interface trade +(finite width). Next: drift (salt fingering + settling, item 3), then double_well tension (oil & water, item 4). + +## Fluids/matter item 3: drift -- settling/separation, and the density-buoyancy fix (+8 tests) + +holographic_mixture.py. drift (first of the two new physics terms): a channel heavier than the LOCAL blend sinks +relative to it (lighter rises), applied as an extra VERTICAL advection by a settling velocity ~ (comp_density - rho). +matter_step's drift hook (shipped off in item 2) now real, exposed via matter_step(drift_strength=...) on the mind. +MEASURED vs a proper drift-OFF baseline: heavy dye (rho 3.0) sinks COM 24.0->22.1 with drift, 24.0->24.0 without; +light dye (rho 0.2) rises 24.0->24.8; stronger drift settles more; two co-located channels separate (heavy below +light -- the immiscible precursor). + +REAL BUG FIXED (probe/measure discipline): buoyancy_force couples DENSITY through alpha, TEMPERATURE through beta; +matter_step passed only beta, so the blended density had NEVER driven convection (vertical KE was 0). Fixed to pass +alpha=mix.buoyancy -> a heavy band now creates flow (KE 0 -> 125). Item 2 tests unaffected (they used buoyancy=0). + +KEPT NEGATIVE (loud): settling + density-buoyancy are clean wins with baselines; resolving DISTINCT salt FINGERS is +NOT -- it is dominated by bulk overturning at this grid/time. The fingering ingredients (differential diffusion + +drift + density buoyancy) are all present and run finite, but the fine-scale instability needs a finer grid and +Rayleigh tuning a fast interactive demo doesn't give. Not claimed as a win. Next: double_well tension (oil & water, +item 4, the miscible<->immiscible dial), then ScatterLayer + ScaleNode, then the performance half. + +## Fluids/matter item 4: double_well tension -- oil & water, the miscible<->immiscible dial (+8 tests) + +holographic_mixture.py. The SECOND new term, closing the dial table. _double_well(phi)=phi(1-phi)(1-2phi) -- FIXED +the shipped phi^3-phi (wells at +/-1) to wells at 0 and 1 for [0,1] volume fractions. matter_step's tension term +rewritten as clean Allen-Cahn sharpening: phi += dt*tension*(-W'(phi) + 0.5*laplacian) -- the double-well pulls each +cell toward a phase, the diffusion sets interface width, tension scales separation strength. FIXED the shipped form +(dw/tension - tension*diffuse) which had HIGH tension -> MORE blending (inverted from the dial). Now high tension -> +sharp. Tension is a Mixture ctor param (already wired); no new faculty. + +MEASURED: a fully-blended 0->1 ramp (committed fraction 0.65) sharpens to 0.94 committed under tension 2.0 but stays +0.65 under tension 0 (the miscible<->immiscible switch); more tension sharpens more; W'(0.3)>0 / W'(0.7)<0 / wells at +0,1 fixed; oil-in-water separates (>0.7 committed) and stays finite. Integration test: oil/water separate with tension, +blend without, through the mind. Items 2/3 regression green (tension path only active when tension>0). + +DIAL TABLE NOW COMPLETE: one advected-field model spans smoke (1ch, tension 0), dye/milk (Nch, tension 0), settling/ +fingering (drift), oil&water (tension high) -- no new solver, just knobs. KEPT NEGATIVE (loud, from Cahn-Hilliard/ +Allen-Cahn literature): diffuse-interface model -> interface is a few cells wide, never a perfect step; plain CH +shrinks tiny droplets (a conservative variant needed when oil volume must stay put). Next fluids items: ScatterLayer +(surface geometry emission) + ScaleNode (cosmic recursive rollup), then the performance half (MC1-3, PW1-4). + +## Fluids/matter item 5: ScatterLayer + ScaleNode -- surface scatter & cosmic rollup (+14 tests) + +Two reuse-first faculties completing the CONTENT half of the fluids backlog. + +holographic_scatterlayer.py: ScatterLayer(instance, count, scale, density, cell_size, seed) emits GEOMETRY onto ANY +surface (not just terrain) by reusing emit_from_surface -- points projected onto the SDF zero-set, importance-sampled +by an optional density map. Each placement = bind(instance, cell_code) (deterministic hashlib cell hash), the whole +layer = a bundle -> region-queryable via recall_region (unbind the cell code). Faculty: scatter_surface on the mind. +MEASURED: 60 grass instances land ON a sphere AND a box (|sdf|<0.05), unit normals; a top-hemisphere density map puts +>90% up top; near cell reads above far. PROBE CATCH: SDF objects aren't callable -> use .eval; region query is +crosstalk-limited (~1/sqrt(N)) so threshold set honestly (near>far, not near>0.1) -- KEPT NEGATIVE: dense scatter +needs the bake+LOD path. + +holographic_scalenode.py: ScaleNode(scene, lod_px) -- 'a parent carries the accumulated value of its children' = the +MONOID. summary(handle) rolls a subtree up (mass SUM, look BUNDLE, leaf count) -- the same reducers +distribute_compute(reduce='sum'/'bundle') uses; draw(handle, apparent_px) returns the summary when small (below LOD) +or descends when big. Faculty: scale_node on the mind. PROBE CATCH: distribute_compute buckets must be iterables with +a worker(bucket, cache) -- it parallelises heavy work, not a scalar reduce; forcing every sum through it was contrived ++ unreadable, so ScaleNode uses plain sum/bundle (the monoid) and the integration test shows distribute_compute gives +the same total. MEASURED: galaxy->systems->planets mass rolls up exactly (42.0 / 12 leaves), look bundles to 256-d, +adding a planet updates the summary by exactly its mass (associative), draw collapses at orbit / descends when big. +Integration test: scatter grass on a planet -> scene of blades -> scale rollup == count -> orbit draws one summary +blob; distribute_compute matches. + +KEPT NEGATIVE: rollup exact only for additive props + bundled look (no exact weather from orbit); atom->galaxy range +needs relative-transform discipline. CONTENT HALF of the fluids backlog now DONE (items 1-5: smoke presets, matter +model, drift, tension, scatter+scale). Remaining: the PERFORMANCE half -- MC1 compile+fuse materials, MC2/MC3 bake +channels, PW1/PW2 pipeline compile, PW3/PW4 iterate sim readout. + +## Fluids performance MC1: compile+fuse materials -- one cached kernel, constants folded (+8 tests) + +PERFORMANCE HALF of the fluids backlog started. holographic_matcompile.py: compiled_shader(material, cache) builds a +material's socket resolution ONCE, keyed by material_spec (constants by value, sockets by stable id), reused for every +hit/instance/frame via the content-addressed compile cache. Constant channels are FOLDED (resolved once, broadcast +per hit) so only procedural sockets re-resolve. _is_const probes a channel at two point sets (position independence); +color routes through surface._rgb, scalars through resolve_param. Faculty: compile_material on UnifiedMind. + +MEASURED: compiled shade matches mat.resolve exactly; a mostly-flat material folds color/reflect/emission/opacity and +only re-resolves 'roughness'; same material -> 1 compile + N cache hits (6 frames = 1 compile, 5 hits); changed color +const -> fresh content-addressed compile; all-constant material has zero per-hit sockets. + +REAL LATENT BUG FIXED: holographic_compile.compiled used `cache or DEFAULT_CACHE`, but an EMPTY CompileCache is falsy +(__len__==0), so a freshly-passed cache was silently ignored and everything went to DEFAULT_CACHE. Fixed to +`cache if cache is not None else DEFAULT_CACHE` (both in compiled() and my compiled_shader). compile regression green. +Also fixed a 1-point resolve_param edge case (resolve const values with 2 points). + +KEPT NEGATIVE: folds constants + caches the BUILD; a fully-procedural material still evaluates every field per hit -- +turning fields into a LOOKUP is MC2 (sdfbake/prt). Next: MC2 bake view-independent channels, MC3 view LUT, then PW1/2 +pipeline compile, PW3/4 iterate sim readout. + +## Fluids performance MC2: bake view-independent material channels -> lookups (+7 tests) + +holographic_matbake.py. MC1 folded constants; MC2 BAKES the position-dependent (view-independent) channels: sample a +material's procedural field over the object bounds ONCE onto a res^3 grid, then per hit trilinearly LOOK IT UP (O(1), +no field re-eval) -- reusing the sdfbake grid idea, pointed at a material channel. BakedField (trilinear 8-corner +sample, scalar or (,3) colour grid); bake_field(field, name, lo, hi, res); bake_material(material, lo, hi, res) -> +shade() of folds + lookups. Faculty: bake_material on UnifiedMind. + +MEASURED: a sin-based procedural roughness + colour ramp baked at res 48 matches the true field to <0.02 (linear +fields reproduce EXACTLY -- trilinear is exact for linear data); constants folded (reflect/emission/opacity), fields +baked (color/roughness); finer grid more accurate (err 0.008 @res8 -> 0.0001 @res64 -- the memory/accuracy trade); +across 5 frames of lookups the field is evaluated ZERO more times (proved by a call counter). Integration test: +baked-vs-direct equal output + zero field evals across frames (fixed a test bug where the per-frame reference resolve +itself called the field, inflating the count). + +KEPT NEGATIVE: bake trades MEMORY for speed + blurs sub-cell detail; pays off in REPEATED sampling not one-shot (the +bake costs one full grid eval up front); only VIEW-INDEPENDENT channels bake -- view-dependent specular is MC3's +(position,view) LUT. Next: MC3 view LUT, then PW1/PW2 pipeline compile, PW3/PW4 iterate sim readout. + +## Fluids performance MC3: pre-integrated view LUT -- specular becomes a table read (+7 tests) + +holographic_viewlut.py. MC2 baked view-INDEPENDENT channels; MC3 handles the view-DEPENDENT specular via 'add a +dimension': bake directional_albedo (a 4096+-sample hemisphere integral) over a (view_cos, roughness) grid ONCE, then +per pixel BILINEARLY look it up -- the classic pre-integrated BRDF / split-sum LUT. ViewLUT(grid, rough_min, +rough_max).sample(view_cos, roughness) bilinear; bake_view_lut(metallic, base_color, res_view, res_rough, samples, +seed). Faculty: bake_view_lut on UnifiedMind. With MC1+MC2+MC3 a material shades as folds + position lookups + a view +table read -- no per-hit field eval, no per-pixel integral. + +MEASURED: lookup matches a fresh 32-65k-sample directional_albedo to <0.058 across roughness>=0.3; reflectance falls +with roughness (0.91>0.41, GGX energy loss); ~11000x cheaper than the integral (2000 lookups vs 2000 integrals); +bilinear exact at nodes; clamps out-of-range. + +PROBE CATCH / KEPT NEGATIVE (honest): finer grid did NOT reduce error -- the cause is that directional_albedo uses +UNIFORM hemisphere sampling, a HIGH-VARIANCE estimator for smooth (low-roughness) surfaces where the BRDF concentrates +near the mirror direction. Both the LUT and a fresh integral are noisy at roughness<0.2 (~0.2). The LUT faithfully +stores directional_albedo; importance sampling would fix the ESTIMATOR, not the table. Also: the table is per +(metallic, base_color) -- a different metalness needs its own bake or a third axis. Next: PW1/PW2 pipeline compile +(per-stage bake + whole-pipeline compile/fuse), PW3/PW4 iterate sim readout (diagonalize linear sub-steps). + +## Fluids performance PW1/PW2: compile the pipeline plan once per config (+9 tests) + +holographic_pipecompile.py. The material compile (MC1-3) one level UP, on the whole pipeline. build_pipeline does real +planning EVERY call (SELECT enabled stages, AUTO-INCLUDE prerequisites, TOPOSORT); across frames of the same config +that plan is invariant, so PW2 compiles it once keyed by the config's content (dataclasses.asdict spec + options), +reusing the content-addressed compile cache. config_spec(cfg, options); compiled_pipeline(cfg, registry, options, +cache); run_compiled(...) the frame-loop entry point. PW1: bake_pipeline(pipeline, scene) runs each stage's optional +bake(scene, seed) ONCE (static view-independent buffers), skipping stages without one. Faculties: compile_pipeline, +run_pipeline on UnifiedMind. + +MEASURED: compiled plan matches build_pipeline exactly; 10 frames of a preview config = 1 plan compile + 9 reuses; +different config or options -> fresh compile; two identical configs share one plan (content-addressed, not identity); +per-stage bake hook runs exactly once; stages without a bake skipped. + +KEPT NEGATIVE: saves the SELECT/TOPOSORT, not per-frame stage work; win scales with frames-per-config (one frame gains +nothing); a stage whose output changes every frame (the render) correctly can't bake. Bit-for-bit same frame as a +direct build_pipeline().run(). Next: PW3 bake-vs-compute per stage (extend adaptive.plan_render), PW4 iterate sim +readout (diagonalize the matter model's LINEAR sub-steps -- diffusion/buoyancy-smoothing -- so any t is a direct +evaluation; nonlinear advection still marches). + +## Fluids performance PW4: iterate sim readout -- diagonalize the linear diffusion sub-step (+9 tests) + +holographic_simreadout.py. iterate is PRT-for-TIME. The matter model's per-channel step is advect(nonlinear) -> +diffuse(LINEAR) -> tension(nonlinear) -> drift(nonlinear). fields.diffuse multiplies by exp(-amount*k^2) in Fourier +space = DIAGONAL bind (eigendecomp is FREE = the rfft). So diffusing for any k steps is ONE transform pair (transfer +to the k-th power), not k marched steps; limit is closed form (non-DC modes decay, mean survives -> flat steady state). +diffusion_transfer(shape, amount); diffuse_at(field, amount, k) [k fractional ok]; diffuse_limit(field). Faculties: +diffuse_readout, diffuse_steady_state on UnifiedMind. + +MEASURED: readout at k=1..20 matches k marched fields.diffuse to 1e-9 (heat semigroup: k diffuse(amount) == +diffuse(k*amount)); fractional steps interpolate; long diffusion -> closed-form mean to 1e-6; mass conserved (DC +transfer=1); k=0 is identity. err 4.4e-16 at k=20. + +KEPT NEGATIVE (the honesty of the item): ONLY the linear time-invariant sub-step diagonalises -- nonlinear advection, +buoyancy coupling, double-well tension still march (adaptive dispatch localises the marching). Same boundary the +dynamics propagator drew (linear-in-Fourier exact; nonlinear needs the reservoir lift). Remaining fluids item: PW3 +bake-vs-compute per stage (extend adaptive.plan_render to decide bake vs march per stage from the workload). + +## Fluids performance PW3: bake-vs-compute per stage -- FLUIDS BACKLOG COMPLETE (+10 tests) + +holographic_stageplan.py. adaptive.plan_render decides bake-vs-analytic for the WHOLE render; PW3 pushes that +break-even down to each pipeline STAGE. Rule (reusing plan_render's _BAKE_MIN_FRAMES): STATIC stage -> bake once +reused across enough frames; static but too few frames -> compute; DYNAMIC stage -> always compute. stage_is_static +reads an explicit .static flag, else a name heuristic (_STATIC_HINTS/_DYNAMIC_HINTS), else CONSERVATIVE dynamic (never +bake blindly -- baking a dynamic stage is a correctness bug). plan_stage / plan_stages / stages_to_bake (feeds PW1). +Faculties: plan_stage_execution, plan_pipeline_bakes on UnifiedMind (compiles the pipeline then plans its stages). + +MEASURED: preview pipeline over 30 frames -> gbuffer BAKES, render/reproject/denoise/present COMPUTE; over 1 frame +nothing bakes; explicit static=False overrides the name heuristic; break-even is exactly plan_render's constant; +reasons human-readable. It's the decision layer over PW1 (bake mechanism) + PW2 (compile mechanism). + +KEPT NEGATIVE: unannotated stage -> conservatively dynamic -> never baked (correctness over speed); a stage opts in +with static=True. + +=== FLUIDS/MATTER/SCALE BACKLOG COMPLETE === +Content half: (1) smoke presets, (2) Mixture+matter_step, (3) drift, (4) double_well tension, (5) ScatterLayer+ +ScaleNode. Performance half: MC1 compile+fuse materials, MC2 bake view-independent channels, MC3 view LUT, PW1/PW2 +pipeline compile, PW3 bake-vs-compute per stage, PW4 iterate sim readout. One advected-field matter model with 3 +dials (smoke->dye/milk->salt fingering->oil&water) + a compile/bake/lookup flattening from material to whole pipeline, +all reusing wired faculties, every kept negative loud. Real bugs caught by measuring vs baselines: alpha density- +buoyancy coupling, double-well well positions + inverted tension, empty-CompileCache-falsy, MC3 estimator variance. + +## CI fix (facade H rebuild) + the build-ergonomics batch (six items) + a stable sigmoid (+13 tests) + +Two things this session: fix the red CI, then work the build-ergonomics backlog. + +**CI fix -- facade H was lost to a filesystem rollback.** test_lecore.py failed 3 ways: `hasattr(lecore,'scene')` +False, and `lecore.scene` / `lecore.areas` missing. The NOTES already record F+G+H as done, but the live +`lecore.py` was back to the bare 22-line stub (UnifiedMind + raw ops only) -- a snapshot rollback ate the facade +code while the NOTES entry survived. PROBED first (the failing test + which module owns each expected symbol), +then rebuilt `lecore.py` faithfully to the recorded H spec: five curated areas as SimpleNamespaces, re-exporting +EXISTING names only -- + scene (Scene, SceneObject <- scene_doc), model (ModifierStack, describe_object <- modifier; sphere, box <- sdf; + extrude_face/inset_face/dissolve_vertex <- meshverbs), render (RenderSession <- session, path_trace <- + pathtrace, CancelToken <- cancel, PipelineConfig <- pipeline), sim (plan_waves/solve_waves <- waveadaptive, + FreeSurface <- freesurface, MPMSnow <- mpm, StableFluid <- fluid, scatter/gather <- transfer), transform (the + whole G kit <- transform). `areas()` derives its map straight off the namespaces (one source of truth, can't + drift). All 3 test_lecore tests green. + +**Build-ergonomics batch (the six requests):** +1. `png_bytes(rgb01, level=6)` in holographic_render.py [core] -- factored the existing save_png encoder body to + RETURN bytes (what every web/demo backend needs); save_png is now a thin wrapper. Pixel output bit-identical + (kept the `*255` truncation, not a +0.5 round). level: 1=fast preview, 6=still. +2. `holographic_sdfscene.py` [core] -- an SDFScene base class: subclass, implement parts() -> [(sdf_fn, mat)], + get .eval (min), .part_ids (argmin), .ids (the alias to_splats expects), material_at for free. Optional + SpatialGrid-backed parts_near() for many-part culling. KEPT NEGATIVE (loud): automatic broadphase for an SDF + *min* needs per-part bounds + an upper-bound pass to prune safely, so the base eval stays naive-and-correct + and culling is opt-in via bounds()+parts_near() -- no silent pruning on the argmin path. +3. `tools/new_demo.py ` [demo-side] -- scaffold generator: stamps backend.py (Blueprint `bp` + /api/draft), + index.html (viewport+slider), demo.json; the generated demo already RENDERS a placeholder and passes smoke. +4. `tools/demo_kit.py` [demo-side] -- png_response/json_response (no-cache) + a self-contained smoke_test that + mounts a demo's `bp` on a throwaway Flask app and checks each GET is 200 / PNG magic. Importing the backend + IS step one, so the module-load NameError pattern surfaces instantly. +5. `apiquickref.py` -> API_QUICKREF.md [core] -- a CURATED, scannable one-line-per-symbol reference for the + app-building surface (scene/model/geometry/transform/camera/render/export), ast-only like docgen. Distinct + from docgen's exhaustive REFERENCE.md. Wired a drift-check step into ci.yml (regenerate + git diff --exit-code). +6. `CONVENTIONS.md` [docs] -- the four load-bearing gotchas in one page: SDF sign (negative inside), colour space + (shade linear, tonemap+gamma last), camera handedness (right = forward x up), determinism trio + (PYTHONHASHSEED=0, hashlib not hash(), stable sorts). Grounded in the live camera/postfx/transform code. + +**Also:** made holographic_deliberate.py's coherence sigmoid numerically stable (branch on sign of z; only exp() +a value <= 0) -- kills the `overflow encountered in exp` RuntimeWarning the CI log showed. Mathematically +identical to 1/(1+exp(-z)) on non-overflowing inputs; ~0 (not a warning) on extreme-negative margins. + +Tests 2998 -> 3011 collected (+13: png_bytes 1, sdfscene 7, build-ergonomics 5). The 3 facade tests now pass. +Demo-side tools live under tools/ here (drop them into the gallery bundle); core items are in the engine repo. + +## Published as leos-core (import name stays lecore) + PyPI publish CI + docs (no test delta) + +Packaging/infra + docs, no new modules or tests. + +- The plain name `lecore` is TAKEN on PyPI (unrelated "Logic Elements core utilities", active). Distribution + name != import name, so we publish as **`leos-core`** (this engine is the core of leOS) and keep `import + lecore` via the shim. Verified end to end: built the wheel, installed it in a clean venv -> `pip show` says + `leos-core`, `import lecore` works. `setup.py` name changed; `lecore.py`/module layout untouched. +- `package.yml` gained a `publish-pypi` job: runs only on a `v*` tag, only after the build+import smoke test, + uses PyPI **trusted publishing** (OIDC, `id-token: write`) so NO token lives in the repo. Optional + `environment: pypi` gate left commented for the simplest first setup. One-time PyPI-side setup (pending + publisher: owner AnOversizedMooseWithSocks, repo leCore, workflow package.yml) documented in PACKAGING.md. +- Docs updated with the no-clone install path: README top now leads with `pip install leos-core` (then a + clone block for hacking/tour), and the extras block shows BOTH `pip install "leos-core[name]"` and the + `pip install .[name]` clone form. PACKAGING.md rewritten (install line, distribution-vs-import-name note, + numbered trusted-publisher walkthrough). build_package.sh output-filename comment fixed to leos_core-*. +- NOT reserved by this: the bare `leos` name (PyPI reserves only the exact uploaded name) -- noted in + PACKAGING.md as a separate placeholder-upload if wanted. Off-repo: discoverleos.com would need the same + install line added by hand (outside this repo). +- The `> Requires: numpy (pip install numpy)` line in REFERENCE.md is a per-module docstring requirement (not + a project install), so it's left alone; researchLog.md pip lines are historical log, left as-is. + +## Auto-calibrating render: wire the convergence machinery into the render loop (+ variance-guided SVGF) [3011 -> 3021 tests] + +The gallery renders were grainy because make_gallery.py called the RAW path tracer at a fixed spp -- grainy in +the hard spots (glass, reflections), wasteful in the easy ones (flat sky), and never touching the pipeline's +denoise/adaptive machinery. Probe-first finding: the convergence machinery ALREADY EXISTED but was siloed -- +`holographic_adaptive_sample.py` (`converged_mask`, `sample_budget`: a calibrated CLT stop rule on path_trace's +per-pixel variance-of-the-mean) was only used as a helper, never driving an actual render loop. So this was a +WIRING problem, not a missing-capability one (the panel's recurring lesson). + +Two wiring fixes, both additive/backward-compatible: +- **Variance-guided SVGF** (`holographic_svgf.atrous_bilateral` gained `variance=None`): the real SVGF move -- + the per-pixel COLOUR edge-stop width becomes `max(color_scale*sqrt(variance_p), floor)`, so noisy pixels blend + freely (grain removed) and CONVERGED pixels collapse to the floor (detail preserved). `variance=None` + reproduces the old fixed-sigma behaviour bit-for-bit. This is what fixed the over-smoothing I kept hand-tuning + around: the denoise strength now CALIBRATES ITSELF from measured noise instead of a global magic sigma/levels. +- **`holographic_gbuffer.render_auto`**: the auto-calibrating render. Samples in PASSES; after each, `converged_mask` + says which pixels hit the target CI -> they stop, the rest keep sampling (path_trace's `active` mask), so hard + pixels (glass/silhouettes/grazing) get more samples automatically. Then variance-guided SVGF. ONE knob: + `quality` (a target CI half-width). Also added `primary_gbuffer` (the missing real G-buffer -- one cheap primary + pass for normal/albedo/depth; the pipeline's g-buffer stage was a synthetic stand-in) and `render_denoised` + (the fixed-spp predecessor, kept + tested). + +MEASURED (spheres, tonemap-space PSNR vs a 128-spp reference -- tonemap space because MC grain is low-amplitude in +raw HDR but shows after the tonemap stretches the shadows): at EQUAL average sample budget, render_auto BEATS a raw +trace at draft/medium (+2.4 dB @ ~10 spp, +0.8 dB @ ~19 spp) and TIES near convergence at high (~-1 dB: once a +pixel is converged, denoise can only soften detail -- the SAME documented crossover the denoise module already +keeps loud). The adaptive sampler concentrates effort: on the spheres scene it spent ~31 mean / 64 max spp, i.e. +~2x on the hard pixels. KEPT NEGATIVE unchanged and reaffirmed: SVGF denoises, it does not add detail; 5-level SVGF +over-smooths (the sweep showed it plateaus low) -- variance guidance is what makes a fixed level safe. + +Wiring/close-out: `render_auto` wired as a UnifiedMind faculty next to `path_trace`; integration test +`test_render_auto_faculty_calibrates_through_unified_mind` runs it through the mind and checks max_spp > mean_spp +(effort actually calibrated). make_gallery.py rewired: every 3-D scene now calls `render_auto(quality="high")` with +NO per-scene spp/denoise numbers (only max_bounce, a physical scene property, stays per-scene). Three NEW showcase +scenes added: `render_identities` (one SDF surface as clay/copper/glass -- the mesh<->SDF<->splat "three costumes"), +`render_fur` (groom + Marschner strand shading), `render_ocean` (SDF Gerstner water in a tank + a wooden cube at its +Archimedes waterline, IOR-1.33 refraction). Tests +10 (gbuffer render_auto/adaptivity/determinism, variance-guided +SVGF backward-compat+calibration, the integration test). tour.py: an AUTO-CALIBRATING RENDER block in the render +section (verified in isolation: 8 passes, 28 mean / 64 max spp). + +## Render lighting & materials: ACES filmic + auto-exposure, HDR sun/sky, dispersion, caustics [+3 tests] + +The renders looked washed out: a flat two-tone sky (no key light, low contrast, nothing bright to reflect) plus a +plain Reinhard tonemap that compresses everything toward grey. Fixed the LIGHTING and the TONE curve, added two +"crank it up" material effects, all backward-compatible additions in holographic_gbuffer.py (+ make_gallery.py): + +- **ACES filmic tonemap + auto-exposure** (`aces_tonemap`): Narkowicz's 2015 ACES curve fit -- filmic contrast and + a graceful highlight roll-off instead of Reinhard's grey mush. AUTO-EXPOSURE meters each scene's log-average + luminance onto mid-grey (0.18, Reinhard's photographic key), so scenes lit by a bright HDR sun self-expose to a + consistent mid-tone with no hand-set stop. Measured: mean pixel dropped from ~0.66 (bright/washed) to ~0.44 with + real blacks (p5~0.13) and controlled highlights (~0% blown) across all scenes. +- **HDR sun/sky environment**: the gallery's flat `_sky` gradients now wrap `holographic_raymarch.sky_dome` (which + already had a bright sun disk + glow + HDRI-env support -- it just wasn't being used) with a bright HDR sun + (radiance >> 1) along one shared SUN direction. Metal/glass now have a high-contrast world to reflect/refract. +- **Chromatic dispersion** (`render_dispersion`): trace R/G/B in three passes, each with a different dielectric IOR + (blue bends most -- Cauchy); take each channel from its own pass. Glass splits white light into a coloured fringe. + 3x cost, for hero glass/water shots. Standard 3-wavelength spectral trick. +- **Caustics** (`add_caustics`): a forward path tracer can't find caustic paths inline (no NEE/photon step), so use + `holographic_globalillum.caustics` (forward light-tracing: shoot light rays, refract through the dielectric, splat + where they land -> the focused cusp) to build a receiver-plane intensity map and composite the above-average + focusing onto the floor pixels BEFORE tonemapping. `caustic_sdf` restricts the refractor to just the dielectric so + opaque props don't spuriously refract. + +Wired into the glass gallery scene (dispersion=0.06 + a glass-only caustic under it). Interesting measured side effect: +with the higher-contrast HDR lighting the spheres benchmark FLIPPED to a clear pipeline win even at high quality +(auto 32.6 dB vs raw-at-38spp 30.3 dB, +2.3 dB) -- realistic bright-sun lighting makes more variance/fireflies, so +the variance-guided denoise earns its keep where flat lighting left it near-neutral. Tests +3 (aces tonemap +bounds/monotonicity/auto-exposure; dispersion splits R-vs-B in the refracted region; caustics brightens floor only, +darkens nothing) -- these also NUMERICALLY verify the two effects fire, since the image viewer was unavailable to +eyeball this pass. KEPT HONEST: caustics are composited (a separate forward light-trace), not found by the camera +path tracer; dispersion is a 3-wavelength RGB approximation, not full-spectral; fur uses its own strand renderer so +it doesn't pick up the ACES/auto-exposure path. + +## Physical materials wired into rendering: PBRMaterial physical props + matlib as the source of truth, fur fixed [+7 tests] + +Two related issues: the fur looked bad, and materials weren't a single physical source of truth for the renderer +(scenes hand-coded parameter tuples; the standard PBRMaterial and the ~130-entry holographic_matlib library were +never consumed by the path tracer, and neither carried the physical dielectric/fiber properties glass and hair +need). Probe-first finding: the render MATH was already physically based (GGX surfaces, Fresnel smooth-dielectric +glass, a real Marschner R/TT/TRT fiber BSDF) -- what was missing was the material DEFINITIONS carrying that data. + +- **PBRMaterial extended with the physical channels the renderer needs** (holographic_materialio, backward-compatible + defaults): `ior`, `transmission`, `attenuation_color`/`attenuation_distance` (Beer-Lambert), and a fiber descriptor + (`fiber`, `fiber_roughness`, `fiber_tilt_deg`). These are the real glTF extensions KHR_materials_ior / _transmission + / _volume, emitted by `to_gltf_dict()` only when non-default (opaque materials export exactly the old entry). So a + material stays glTF/USD/Blender-Principled interchangeable AND now carries the physics. +- **holographic_matlib.material() now populates the physics by class/name**: glass/gem/liquid presets get + transmission=1 + a real IOR (glass 1.5, diamond 2.42, water 1.33, ...) + an attenuation tint; a new `fiber` class + (fur_brown/ginger/gray, hair_blonde/black/brown/red) gets Marschner strand params. Added the renderer adapters + `shade(mat, n)` (-> the path tracer's (albedo, metallic, roughness, emission, ior) tuple, routing transmission vs + opaque) and `fiber_params(mat)` (-> the hair shader's colour/roughness/tilt). The library is now the single + physical source of truth the renderer reads FROM. +- **render_hair reads physical fiber params** (holographic_hairshade): `roughness` -> longitudinal lobe width beta_r, + `tilt_deg` -> cuticle tilt alpha_r, forwarded to marschner. Backward-compatible (None -> old defaults). +- **Gallery wired to the library**: the identities scene ("one surface, three materials") now pulls clay/copper/ + glass_clear from matlib and reads their physical props (a `_put` helper writes metallic/roughness/ior per region); + the fur scene uses the `fur_ginger` fiber material with a denser/longer groom, RIM lighting from behind (fur reads + best backlit -- the translucent TT/TRT edges glow), a gradient background, and the ACES tonemap. The old fur was a + dark blob with a blown-out center; the new one has real dark fur (p5~0.02) with bright rim highlights (p95~0.95), + 0% blown. + +Tests +7 (matlib: physical dielectric/fiber props, shade() routing, fiber_params refuses non-fiber; materialio: +PBRMaterial physical extensions + glTF KHR export, opaque stays backward-compatible). KEPT HONEST: the path tracer +still applies the transmitted tint per interface (a Beer-Lambert-ISH coloured-glass term), not a true distance-based +volume absorption -- attenuation_distance is carried in the material and exported to glTF but the CPU tracer uses the +tint approximation; fur uses its own strand renderer (not the path-traced environment), so it's rim-lit rather than +lit by the scene's HDR sun. The material MODEL is now complete and standard; wiring every gallery scene through +shade() (spheres/ocean still hand-assign a few regions) is a mechanical follow-up. + +## Ocean render fixed: a dedicated water shader (Fresnel + Snell refraction + Beer-Lambert depth + pool caustics) + +The ocean render was bad -- blown-out sky/ground with almost-black water, no visible refraction or depth. Root +cause: the water was rendered through the path tracer's smooth-dielectric glass path, which is a CLOSED-object +model (enter one face, exit the far face). An open water surface sitting over a floor doesn't fit that, so the +refraction broke and the water crushed to black; meanwhile the bright studio sky filled the frame and the +auto-exposure metered to it, blowing the top and bottom out. + +Replaced render_ocean with a dedicated, readable WATER shader (the standard water model, ~90 commented lines, +vectorised NumPy -- renders in <1s, no path tracing): + * a look-DOWN camera so the frame is mostly water, not sky (kills the blown-sky problem); + * at the rippled surface (a summed-sinusoid height field with its ANALYTIC normal), FRESNEL (Schlick, water + F0~0.02) splits the ray into a sky REFLECTION and a REFRACTION into the water (Snell, eta=1/1.33); + * the refracted ray is marched to the sandy floor (or the cube's submerged side), and the colour it returns is + faded by BEER-LAMBERT absorption over the underwater distance -- sigma is largest in red, so deep water reads + blue and dark: that IS the volumetric depth/'fog' (the same extinction term holographic_volint.render_fog + uses, applied along the underwater path); + * POOL CAUSTICS via holographic_globalillum.caustics (sunlight focused through the wavy surface) brighten the + sand; a wooden cube floats at its Archimedes waterline; a sharp sun glint sparkles off the ripples. +MEASURED (stats, since the image viewer was unavailable to eyeball): water region now reads blue/cyan +(R 0.25 < G 0.48 < B 0.50, physically correct) with real variation (std 0.17 from refraction/caustics/ripples), +0% black; the sky strip and the whole frame have 0% blown pixels (was an almost black-and-white image before); +overall p5 0.18 / p50 0.36 / p95 0.71 -- a proper tonal range. +KEPT HONEST: this is a single-bounce water shader (reflection + one refraction to the floor), not full multi-bounce +path-traced water; it's the right tool for water-over-a-floor, and it's a SEPARATE path from render_auto (which +stays the auto-calibrating path tracer for the closed-object scenes). + +## Fur grooming fix: comb the strands, supersample for AA, key+rim lighting + +Feedback on the fur: it looked better but (1) stood almost straight out instead of being groomed/styled, (2) was +pixelated to where it was hard to read, and (3) the lighting was strange (a blotchy warm rim + a blown-out white +centre). Root causes, all in make_gallery.render_fur (holographic_groom/hairshade are unchanged except last turn's +fiber params): +- STANDING OUT: groom() grows each strand along the surface NORMAL (its `lean` uses an arbitrary per-strand + tangent, so it can't comb coherently). Added a readable `_comb()` that reshapes each strand to curve from its + outward normal at the root toward a world FLOW direction (down + back) at the tip, projected onto the surface + tangent plane, with a little `lift` so it keeps loft and doesn't clip into the body. Verified numerically: tip + direction went from tip.normal=+0.93 / tip.flow=+0.05 (straight out) to tip.normal=+0.73 / tip.flow=+0.62 (lies + along the body and flows), 0/400 strands clipping inward. +- PIXELATION: the strand rasteriser (_draw_segment) draws 1-px DDA lines, no AA. Render at 2x and box-downsample + (supersampling) -> smooth fur instead of pixel noise. +- LIGHTING: the old single back-light gave the orange/blown look. Now two passes -- a KEY light (front-upper 3/4, + reveals the groomed form) plus a softer warm RIM from behind (edge glow) -- summed, with the Marschner `reflect` + lowered to 0.06 so no blown highlight. Composited over a soft gradient background and ACES-tonemapped. +MEASURED (stats; the image viewer was still unavailable to eyeball): 0% blown pixels (was a blown white centre), +balanced tones p5 0.06 / p50 0.56 / p95 0.68. ~32s at 240x180 (2x supersample, 5000 strands, key+rim = 2 passes). +KEPT HONEST: still the REST groom + a procedural comb (not simulated dynamics or a captured scan), and it uses its +own strand renderer, so it's lit by its own key/rim rig rather than the scene's HDR sun/path tracer. + +## Fur density: enough coverage that the body no longer shows through + +Feedback: the groom looked/styled/lit better, but too SPARSE -- you could see the body through the coat. The honest +fix is simply more fur (render cost IS the strand count, so density is the lever). render_fur now grooms a DENSE +coat: 16000 long strands + 8000 short UNDERCOAT strands (length 0.30) that fill the base, both with length_jitter +for natural variation, combed and rendered as before. MEASURED coverage (fraction of the critter region that is fur +vs. background showing through) rose to 0.93 -- the remaining ~7% is mostly the silhouette edge, not interior gaps. +COST: ~230s at 240x180 (24000 strands x key+rim = 2 passes x 2x supersample), because the strand rasteriser draws +1-px DDA lines in a Python loop -- that's the price of coverage with this renderer. If faster iteration matters more +than the last bit of density, the levers are the strand counts (top of render_fur) or dropping the rim pass; a real +speedup would be vectorising _draw_segment (batch all segments), a heavier follow-up. Image viewer still unavailable +to eyeball, so coverage/exposure were checked by stats. + +## Fur brushed down / relaxed: gravity droop + flatter comb + +Feedback: making progress, but the coat still stuck out fairly straight, leaving spots where the body showed. Added +a gravity `droop` term to _comb (a downward sag that grows as t^2 toward the tip -- the coat RELAXES/settles) and +brushed it flatter (lift 0.40->0.16, bend 1.1->1.45). Verified by geometry: the strand tip went from tip.normal ++0.74 (standing off) to +0.49 (laid down), average loft off the surface 0.41->0.30, with 0/2500 strands clipping +into the body. Result: the INTERIOR core gap fraction (bright background showing through the middle of the critter) +is 0.2% -- the surface no longer shows through. The whole-silhouette coverage metric read slightly lower (0.93->0.88) +only because flatter fur makes a tighter silhouette (less outward spike), not because of interior holes. All the comb +knobs (flow direction, lift, bend, droop) are commented one-liners at the top of render_fur for easy tweaking. +Image viewer still unavailable here, so this was tuned by geometry + coverage stats. + +## Render pipeline wiring, batch 1: pipeline renders REAL scenes; tonemap dedup; smoke&fire showcase [+4 tests] + +Knocking down the RENDER_PIPELINE_BACKLOG. This batch (the "use what already exists" tier): +- J1 (bench): new benchmarks/bench_render.py -- path_trace px-samples/sec, render_auto time-to-quality (+PSNR vs + equal-budget raw), render_hair segments/sec, StableFluid steps/sec, volume_render samples/sec. Baseline recorded + in benchmarks/README.md. This is the speed-regression guard for all the pipeline rewiring. +- A1 (the big one): holographic_pipeline's gbuffer/render/svgf stages were demo stand-ins (a 24x24 _demo_scene). + Added a RenderSpec dataclass; pass one as run(scene=RenderSpec(...)) and those stages run the REAL machinery -- + primary_gbuffer + a new converge_samples() (the sampling half of render_auto, extracted so both share one loop) + + variance-guided SVGF. Verified the staged pipeline composes the SAME frame render_auto returns, bit-for-bit. + None/plain scenes still hit the demo path unchanged (backward compatible; 15 old pipeline tests still green). +- A4: the pipeline's SVGF stage now receives the render stage's per-pixel variance (variance-GUIDED), not a fixed + sigma. +- E1 (dedup): holographic_postfx already owned aces/reinhard/exposure/gamma. The gbuffer aces_tonemap was a + duplicate of the Narkowicz fit -- now a thin delegate to postfx (verified bit-for-bit identical to the old inline + math, auto on and off). Added auto_exposure (log-average->mid-grey metering) to postfx so it lives with the curves. +- E2: PipelineConfig gained postfx="off"|"aces"; the present stage grades via postfx when "aces" (default stays the + legacy inline Reinhard, byte-identical). Validation rejects bad values. +- I1 (paired showcase): render_smoke_fire in the gallery -- ONE 3-D Stable-Fluids plume, rendered twice through + volume_render (smoke = grey absorption; fire = the hot density*temperature core through the blackbody ramp). The + sim and the volume renderer both existed but were never composed into a frame; now they are. +Tests +4 (pipeline renders a real scene matching render_auto; demo path byte-identical; postfx aces grades + rejects +bad config; the E1 aces bit-identity check). KEPT HONEST: the pipeline's adaptive_samples/reproject/splat stages are +still demo stand-ins (not in this batch); the fire glow is a modest plume-base core (physically it's smoke above, +fire at the base), not a bonfire; image viewer still unavailable so smoke/fire tuned by stats. + +## Render pipeline wiring, batch 2: low-discrepancy AA, materials drive spheres+glass, sun-sky default [+1 test] + +Continuing the RENDER_PIPELINE_BACKLOG cheap-wins tier: +- H1 (low-discrepancy anti-aliasing): the camera shot every sample through the pixel CENTRE, so more samples cut + noise but never smoothed edge jaggies. Added an opt-in `antialias=` to path_trace: each sample jitters the ray + to a sub-pixel position from holographic_lowdiscrepancy.low_discrepancy (Roberts' R-sequence -- even coverage, + unlike clumpy plain random). Cameras gained an optional ray_dirs(w,h,jitter=(dx,dy)); a camera that doesn't + support it falls back to centre rays. Default OFF in path_trace = byte-identical to before; ON in render_auto + (default) so the gallery gets AA. Verified: default off is byte-identical, on lowers edge total-variation + (354->344 at equal spp). Threaded antialias through converge_samples + render_auto. Also made the gallery's + raw-vs-pipeline BENCHMARK use antialias on BOTH the raw baseline and the reference, so the PSNR comparison stays + apples-to-apples (it had briefly gone unfair: render_auto AA vs a centre-ray reference). +- B1 (materials drive the render): render_spheres and render_glass now pull physical materials from holographic_matlib + (gold/plastic_red/plastic_blue/glass_clear, checker floor from matte_white/black) via a small _put helper, instead + of hand-typed tuples -- joining render_identities which already did. Only render_ocean still hand-codes, and it uses + a bespoke water shader anyway. +- D1 (sun-sky default): confirmed the gallery _sky/_sky2 already wrap holographic_raymarch.sky_dome (bright HDR sun + + graduated sky), which is the default environment for every path-traced scene. +Tests +1 (the antialias opt-in: default byte-identical, on differs and stays valid). Render bench unchanged vs the +recorded baseline (render_auto in the bench uses antialias=False to stay comparable). KEPT HONEST: AA is sub-pixel +jitter only (no reconstruction filter beyond the box average of the samples); ocean still hand-codes its water. + +## Render pipeline wiring, batch 3: the renderer consumes the canonical SCENE DOCUMENT (H7) [+6 tests] + +The gallery hand-built a bespoke Python `class Scene` per scene, tangling geometry and material logic, while the +engine's canonical mutable document (holographic_scene_doc.Scene -- objects with stable handles, transforms, SDF +geometry, materials, undo, selection, change-events) was never consumed by the renderer. New bridge module +holographic_scene_render.py closes it: +- scene_to_render(scene) -> (sdf, material_fn). Geometry: each object's SDF is PLACED by its transform (translation + + uniform scale read off the 4x4, applied via the SDF tree's own .translate()/.scale()) and UNION-ed into one + scene SDF (nearest-object distance). Material: at a point we argmin over the objects' |distance| to find which + object owns the nearest surface, then shade with that object's library material via matlib.shade -- the same + "closest wins" rule the union uses for distance, applied to appearance. +- render_scene_document(scene, camera, ...) -> renders it through render_auto in one call. +- Wired as UnifiedMind faculties render_scene_document + scene_to_render (anti-silo), with a cross-faculty + integration test (scene_doc + matlib + render_auto meet) and a tour block. +Rendered a 4-object document (floor/red-plastic/gold-metal/jade-box) end to end, materials read straight off the +document. Tests +6 (flatten=nearest-object; material_fn picks the owning object's material; transform places +geometry; empty scene raises; end-to-end deterministic render; the UnifiedMind integration). KEPT HONEST: honours +translation + uniform scale (what the scenes use); full rotation would compose the SDF's .rotate() too -- a noted +extension, not silently wrong. The material argmin is over |distance| so it's correct AT surfaces (where the +renderer shades); deep inside an object a different object's surface can be nearer, which is irrelevant to the +render but caught me in a test (query surface points, not interior centres). + +## Render pipeline wiring, batch 4: subsurface scattering into the path tracer, driven by the material (H2) [+3 tests] + +The subsurface term (raymarch.subsurface: Beer-Lambert on the SDF interior toward the light -> thin regions glow) +existed but was only used by the RASTERISER. Wired it into the PATH TRACER, switched on by the material: +- path_trace._unpack_mat now accepts an optional 6th material element, a per-point SUBSURFACE strength (4/5-tuple + callers unchanged). When present + a new sss_dir is given, a hit gets an added glow = sss * transmit(thinness) * + albedo -- like emission but modulated by how thin the object is toward the sun. Default sss_dir=None => byte- + identical to before (verified). Threaded sss_dir/depth/sigma through converge_samples + render_auto. +- PBRMaterial gained an `sss` field; matlib.material() populates it for translucent presets (wax 1.0, skin 0.9, + jade 0.8, marble 0.5, milk/honey/flesh/leaf). IMPORTANT: a named SSS material is a translucent SOLID, so it now + takes precedence over the gem/liquid-class transmission -- jade/marble render as glowing solids, not refractive + glass. matlib.shade returns the 6-tuple for those; the scene-render bridge's material_fn carries the sss channel + through and only emits a 6-tuple when some object is translucent. +- Wired end to end: render_scene_document gained sss_dir; the UnifiedMind faculty forwards it. Gallery demo + render_subsurface (wax/jade/marble backlit in a dark room, built on the scene document) + tour block comparing + translucent wax vs opaque clay (1.2x brighter, real added light). +Tests +3 (sss opt-in: default byte-identical, on glows brighter; the matlib sss routing; the UnifiedMind integration +wax>clay). KEPT HONEST: this is the field's approximate single-direction translucency (interior path length toward +ONE light), not full multiple-scattering diffusion SSS -- it captures the thin-glow look, the kept-negative the +raymarch.subsurface docstring already states. + +## Fix: subsurface demo simplified to ONE big bumpy blob (was 3 tiny shapes) +The first render_subsurface used three even-thickness blobs (two spheres + a rounded box), so the thin-glows-more +effect had no thin geometry to reveal -- it looked like flat translucent balls. Rebuilt with shapes that have real +thickness variation: a WAX torus (thin ring glows through), a DISPLACED jade sphere (thin bumps light up), and a +MARBLE box carved to a THIN SHELL (walls glow, thick base dark). Measured thin/thick contrast now present (bright- +glow frac 0.33 alongside mid-dark frac 0.42). Lesson kept: an effect demo must include the geometry that EXHIBITS +the effect, or the measurement is right but the picture lies. + +## Render pipeline wiring, batch 5: thermal emission -- hot materials glow by their temperature (H2 cont.) [+3 tests] + +Second physical-property-drives-material item (after SSS). A material now has a temperature_K; a hot material EMITS +blackbody radiation whose COLOUR is Planck's law for that temperature (holographic_blackbody.blackbody_rgb, already +in the tree) and whose BRIGHTNESS ramps with temperature. Implemented in matlib.shade: T>500K adds col*bright to +emission (col = blackbody_rgb(T), bright = ((T-500)/1500)^2). New matlib.heat(name_or_mat, T) returns a heated copy. +Emission already flows through path_trace's emissive term, so NO tracer change was needed -- the physical property +just feeds the existing path. Verified monotonic: iron 700K->2900K emission lum 0.006->1.63, every step red>=green>= +blue (correct cooler=redder hue). Showcase render_hot_metal (five iron bars heated 700..2800K, a heat-gradient row +in a dark room) + tour block + UnifiedMind-path integration test (hot iron glows warm vs cold iron dark). Tests +3. + +ALSO fixed this batch: the subsurface demo (batch 4) used even-thickness blobs so the thin-glows effect couldn't +show; rebuilt with varied-thickness shapes (wax torus / displaced jade sphere / hollow marble shell). Lesson: an +effect demo must contain the geometry that exhibits the effect. + +## Fix: subsurface demo -> ONE large displaced blob +Three shapes were too small to read at gallery size. Dropped the torus and hollow shell; kept a single big +displaced wax sphere filling the frame (bumpy surface = varied thickness all over; thin ridges glow, thick body +dark). Fills 100% of frame, ~47% bright-glow / ~53% dark-thick. Simpler and legible. + +## Render pipeline wiring, batch 6: physical-structure materials (crystal grains + ore inclusions) into albedo (H2 cont.) [+3 tests] + +Third physical-property-drives-material item. crystal_material (Voronoi grains, holographic_cellular) and +material_inclusions (calibrated impurity pockets, holographic_inclusions) already returned albedo SOCKETS +f(points)->(M,3) rgb, but nothing fed them to the renderer (its material(P) returned a flat colour per object). +Wired them via the scene document: a scene object can carry overrides={"albedo_socket": socket}; scene_to_render's +material_fn samples that socket at the hit points to override the flat base albedo (per-owner, so mixed scenes +work). No socket -> flat material colour, unchanged. Showcase render_crystal (a polycrystalline gem + an ore +boulder with gold/iron pockets) + tour block + UnifiedMind integration test (the gem renders with facet colour +variation, not flat). Tests +3 (socket varies per-point; no-socket stays flat; the mind-path render). This is the +same "material carries physics, shader reads it" pattern as SSS (batch 4) and thermal (batch 5), now for spatial +structure -- structure IS the texture, deterministic and volumetric, no image maps. + +## Fix: subsurface demo -> ORANGE honey + two lighting bugs found and kept +The white wax blob was unreadable (white glow on grey). Switched to honey (orange translucent) so the glow has a +colour: thin bumps blaze orange, thick body absorbs to black. Debugging surfaced two real bugs worth keeping: +(1) the "backlight" was pointed AT the camera -- sun_dir and the SSS march direction were +z while the camera sat +at +z, i.e. a front light labelled back; a true backlight is -z, and a DEAD-ON backlight then blows the background +white (camera stares into the halo) -- the working setup is SIDE-BACK, well off the camera axis. (2) the gallery's +auto-exposure lifts a deliberately dark room to mid-grey, washing out exactly the dark-vs-glow contrast the demo +exists to show -- this shot uses FIXED exposure (aces_tonemap(auto=False)). Also threaded sss_depth/sss_sigma +through render_scene_document + the UnifiedMind faculty (they were stuck at the render_auto defaults). Final: +blob ~29% orange glow (hue 0.79/0.65/0.20), background 0.12, nothing blown. + +## Fix: SSS contour banding -- march quantization, dithered +Moose spotted contour bands on the subsurface blob. Cause: raymarch.subsurface samples the interior at 10 fixed +positions, quantizing the measured thickness to depth/10 -- on the demo geometry that collapsed a smooth thickness +ramp to 10 distinct transmit levels (7 visible plateau bands on a scanline). Fix, two parts: (1) subsurface() gained +an optional per-point `jitter` (a [0,1) fraction shifting that point's sample offsets by a sub-step -- jitter=None +is byte-identical to before); (2) path_trace's SSS term now uses enough steps to keep the quantum ~0.035 world +units AND dithers with a deterministic hash of the hit position (sin-dot-product hash -- no RNG state, fully +reproducible), so residual quantization becomes fine noise the SVGF pass smooths. Measured A/B on the demo's real +geometry: 10 -> 30 distinct levels, 7 -> 4 plateau runs (the remainder are genuinely equal-thickness flats). Test +pins jitter=None identity + the level improvement. Cost: 37 sdf evals instead of 10, only on translucent hits. + +## Render pipeline wiring, batch 7: VOLUME as a first-class pipeline stage (H5) [+5 tests] + +Smoke/fire/fog SIMULATED in the pipeline but the volume RENDER never reached a frame -- the smoke/fire demo +hand-composited it outside the pipeline. Fixed: RenderSpec gained an optional `volume` dict (field/bounds/mode/ +sigma/steps/emission_color/background); a new pipeline VOLUME stage (_volume_run) renders it with +holographic_render.volume_render and over-composites onto the surface image: out = volume + surface*(1-alpha). +Registered as Stage("volume", needs=("image",), produces=("composited",), enabled=cfg.volume); phase ordering puts +it render -> svgf -> volume -> present. Default OFF (volume=False + scene.volume=None), so every existing pipeline +is byte-unchanged; a volume=True flag with no scene.volume safely no-ops. Showcase render_smoke_over_scene (a plume +rising behind two library-material spheres, composited BY THE PIPELINE) + tour block + UnifiedMind integration +(render_pipeline('final', volume=True).run(scene-with-volume)). Tests +5 (composites over surface; ordering + +default-off; safe no-op; the mind-path). + +Sim-tuning lessons kept: StableFluid's UP axis defaults to axis 0 (X), NOT Y -- the working smoke_fire recipe uses +that default, so the plume rises along grid-X; the demo transposes (1,0,2) to map sim-up -> world-Y. A plume is a +CONCENTRATED column (centre alpha >> edge alpha ~ 0), verified by measuring the alpha's spatial spread, not a grey +wash -- the naive "grey-pixel fraction" metric counts floor+sky and lies. + +## Render pipeline wiring, batch 8: PARTICLES as a first-class pipeline stage (H6) [+15 tests] + +The particle sim (holographic_integrate.ParticleSim advances an (N,3) position array, even stashing it in +ctx.buffers["particles"]) had no RENDERER -- simulated points never became a picture. Built the missing piece: +NEW module holographic_pointsplat.py projects a point cloud through the camera's own view/projection matrices and +splats each point as a soft round Gaussian dot, returning (image, alpha) -- the same contract as volume_render. +Painter's order (far->near) so nearer sparks occlude farther; optional depth_fade dims receding points; fully +deterministic (no RNG). Then a pipeline PARTICLE stage (_particles_run): RenderSpec gained a `particles` dict +{points, colors, radius_px, intensity, depth_fade}; the stage over-composites the splatted layer onto the surface +(out = points + surface*(1-alpha)). Registered after the volume stage (a spark in front of smoke reads right) -- +this required an explicit phase renumber: compositing stages volume(2), particles(3), present(4), because +svgf_denoise produces "denoised" (not "image") so the compositors don't depend on it and were sorting by name into +the wrong slot. Default OFF, byte-unchanged when unused; particles=True with no scene.particles safely no-ops. +Showcase render_sparks_over_scene (an ember swarm over two spheres on a dark floor, composited BY THE PIPELINE) + +tour block + UnifiedMind integration. Tests +15 (7 pointsplat module: projection/occlusion/depth-fade/empty/ +determinism; 3 pipeline: composite/ordering/safe-no-op; 1 mind integration; plus the module selftest). + +Demo lesson kept: too many particles in a small volume project into overlapping CLUMPS, not distinct sparks (320 +pts -> 6 merged blobs, one 2793px). Fewer + wider-spread reads as embers (90 pts -> 46 distinct spark-sized dots). +Measure distinctness by connected-component blob sizes on the spark-only layer, not a bright-pixel fraction. + +## Render pipeline wiring, batch 9: HAIR/FUR as a first-class pipeline stage (H4) [+7 tests] + +Third and last render-STAGE wiring (after volume H5 and particles H6). render_hair drew strands into its OWN image +over an OPAQUE background and returned only that image -- so it could not be a layer over another render. Added an +opt-in return_alpha: _draw_segment optionally marks a coverage buffer at every pixel it paints, and render_hair +returns (image, alpha) when asked. Default (return_alpha=False) is byte-identical -- verified the image is unchanged +and the alpha exactly matches the painted pixels. Then a pipeline HAIR stage (_hair_run): RenderSpec gained a `hair` +dict {strands, shader, hair_color, light_dir, roughness, tilt_deg, lod_stride, smooth_levels}; the stage +over-composites the shaded strands onto the surface (out = hair*alpha + surface*(1-alpha)). Registered LAST of the +layer stages: volume(2) -> particles(3) -> hair(4) -> present(5), so a strand in front of smoke/sparks reads right. +Default OFF, byte-unchanged when unused; hair=True with no scene.hair safely no-ops. Showcase render_fur_over_scene +(a groomed creature coat composited over a PATH-TRACED skin-material body, Marschner look from fur_ginger's fiber +params) + tour block + UnifiedMind integration. Tests +7 (1 module return_alpha back-compat + coverage-matches- +painted; 3 pipeline composite/ordering/safe-no-op; 1 mind integration; + the existing selftest still green). + +The three layer stages now share one shape: simulate/build -> render to (image, alpha) -> over-composite. A scene +carrying any of volume/particles/hair renders as one finished frame; before, each was a standalone renderer whose +output a demo had to hand-composite. Coverage measured body-shaped (central-column warm fur 0.76 vs edge 0.004), +confirming a real coat, not scattered noise. + +## Render build, batch 10: THIN-FILM IRIDESCENCE (soap bubble / oil slick) -- the one MISSING material [+14 tests] + +The first genuine BUILD after the render-stage wiring trio (not wiring -- new capability). Soap-bubble / oil-slick +iridescence: a thin transparent film reflects light off its top and bottom, the beams interfere, and whether a +colour reinforces or cancels depends on film thickness AND view angle -> the hue sweeps across a curved surface. +NEW module holographic_thinfilm.py computes it from first principles: interference_reflectance(thickness_nm, +cos_theta, n_film) gives the two-beam reflectance spectrum (Snell for the in-film angle, OPD = 2*n*d*cos_t, half- +wave flip, R(lambda) = sin^2(pi*OPD/lambda + flip/2)); spectrum_to_rgb integrates it against the CIE colour-matching +curves REUSED from holographic_blackbody (no second colour table); thin_film_tint combines them; iridescent_socket +builds a view-dependent albedo socket. The core is vectorised over per-point thickness AND per-point angle together. + +Wired into the renderer following the SSS pattern: _unpack_mat gained a 7th value = iridescence film thickness (nm, +0 = none; 4/5/6-tuple callbacks unchanged), and the path tracer, where irid>0, computes the tint from the faced +normal and view direction (-Dh, both already in hand at the hit) and multiplies it into the albedo before the BRDF +bounce. PBRMaterial gained iridescence_nm; matlib.shade emits the 7-tuple for iridescent materials; three named +presets (soap_bubble 300 nm, oil_slick 420 nm, beetle_shell 250 nm) via a new _IRIDESCENT table; matlib.iridesce( +name_or_mat, nm) gives any material a film. scene_render's material_fn carries the 7th value through too. + +Showcase render_iridescence (a soap bubble + an oil-slick sphere against a colourful sky -- iridescence tints +REFLECTED light so the environment needs colour). Measured: a 320 nm film shifts colour by 0.97 (RGB) head-on vs +grazing; the soap sphere spans 5/12 hue bins, the oil sphere 6/12 -- a real rainbow sweep, not a flat tint. +Saturation is gentle (~0.15-0.18), which is physically correct -- soap iridescence is a pearlescent shimmer, not +neon. Tour block + UnifiedMind integration. Tests +14 (8 thinfilm module: spectrum range / thickness-cycle / +angle-shift / per-point / fringe-density / socket / determinism / phase-flip; 2 matlib preset+helper; 1 pathtrace +view-dependence; 1 mind integration; + the module selftest). + +Kept note: iridescence tints REFLECTED light, so it only reads against a colourful environment and on a curved/ +varying surface -- a flat grey sky gives almost no sheen (measured hue spread collapses). Not a bug, the physics. + +## Render build, batch 11: PLACED LIGHTS + NEXT-EVENT ESTIMATION -- the tracer's own kept-negative, closed [+9 tests] + +The path tracer's docstring flagged its own gap: "No NEXT-EVENT ESTIMATION / no explicit light sampling: light is +gathered only when a bounce ray happens to hit the emissive ENVIRONMENT... NEE/MIS-with-lights is the honest next +step." Closed it. NEW module holographic_lights.py: PointLight (1/r^2 falloff, hard shadows), DirectionalLight +(sun, no falloff), SphereLight (has AREA -> samples a random surface point each call -> SOFT shadows / penumbra), +and direct_lighting(sdf, P, N, V, mat..., lights, rng) -- the NEE evaluation: per light, point a shadow ray at it +(sphere_trace), if the first hit is farther than the light the point is visible, add f_r(cook_torrance)*L*cos. + +Wired into path_trace via a lights=None param: at each diffuse hit, direct_lighting adds the DIRECT term BEFORE +the existing random BRDF bounce (which still runs, carrying indirect light -- nothing lost). No double-counting: the +lights are ANALYTIC objects, not SDF geometry, so bounce rays can't also hit them. Threaded lights through +converge_samples, render_auto, render_scene_document, RenderSpec + the pipeline render stage, and the mind's +render_scene_document faculty (render_auto uses **kw, so lights flows through). lights=None is byte-identical to the +old environment-only render (pinned by a test). + +Showcase render_lit_scene (three objects in a dark room lit by a warm SPHERE key + a cool POINT rim). Measured: with +only a dark sky the scene is ~black (mean 0.016); one point lamp lights it to 0.099 (~6x) with real shadows (46% +of frame in shadow via direct path_trace; the floor min drops to 0.03 under objects). SphereLight's sampled +directions vary (std ~0.5) -> soft shadows. Tour block + UnifiedMind pipeline integration. Tests +9 (6 lights module: +falloff/directional/NEE-lit-and-shadowed/backfacing/sphere-samples/no-lights; 2 pathtrace NEE+backcompat; 1 mind +pipeline integration). This is foundational: every future render can now use real lamps, not just an environment. + +## Render build, batch 12: THE FULL LIGHT RIG -- 3 light types -> 8, plus fields & gobos [+16 tests] + +Moose flagged the light list as thin. Expanded holographic_lights.py from 3 types (point/directional/sphere) to the +full set a real DCC app has: PointLight, DirectionalLight, AmbientLight (a no-shadow fill -- albedo*colour, handled +specially in direct_lighting), SpotLight (a cone with smoothstep inner/outer falloff + an optional GOBO -- a +projected light cookie f(uv)->multiplier over the cone cross-section), RectLight (a one-sided area softbox, soft +shadows, optional gobo), SphereLight (unchanged), MeshLight (an emissive triangle mesh: area-weighted triangle pick ++ sqrt-barycentric point sample, one-sided), and IESLight (a real luminaire beam shape from a candela profile -- +callable or array interpolated over angle). load_ies(text) parses a standard IESNA LM-63 .ies file (readable subset: +vertical angles + first horizontal plane's candela; ignores TILT geometry and multi-plane azimuth, noted honestly). + +FIELDS: every light's colour AND intensity may be a constant OR a callable f(P)->values, so a lamp can have a +colour gradient or an intensity map that varies across the scene (helper _resolve / _emit). The three original +lights keep their exact constructor + sample() contract (the tracer and old tests depend on it -- pinned by a +back-compat test), so nothing in path_trace changed; direct_lighting stays generic over all eight via the shared +sample() -> shadow-ray -> BRDF path, with the ambient branch the one special case. + +Fixed a real coordinate issue found by the gobo selftest: for a straight-down spot the vertical-beam fallback +up-vector was (1,0,0), which put the gobo's u-axis on world -z, so a pattern's horizontal didn't line up with world +x. Switched the vertical fallback to (0,0,1) so a downlight's gobo u maps to world x (what an artist expects). + +Showcase render_light_types (a gobo spot casting striped bars on a backdrop + a soft rect light + a cool IES +downlight over pillars). Measured: spot on-axis 4.44 vs 0.0000 outside the cone; gobo lit-half > blocked-half; IES +on-axis > off-axis; colour-field lamp reads redder to the right; gobo projects 5 visible stripe bands on the +backdrop; warm/cool split left(+0.022 R-B) vs right(-0.052 R-B). Tour block + UnifiedMind pipeline integration. +Tests +16 (falloff/directional/NEE/backfacing/no-lights/ambient/spot-cone/gobo/rect-one-sided/sphere/mesh/IES- +profile/load_ies/colour-field/intensity-field/back-compat). + +## Render build, batch 13: THE CACHED DOME (RENDER-DC1) -- the soft dome as a three-tier cache [+6 tests] + +The dome (soft sky-ambient / AO) was the render backlog's worst cost: brute-forced it needs many ray-traced AO +samples per pixel per bounce and is still noisy (the showcase dome was 323s). It is also the SOFTEST light, which a +long design thread (with Moose) established is exactly where a cache wins -- measured, not assumed. Shipped +holographic_domecache.py: a three-tier cache, same shape as anim's frame cache, on light instead of frames. + * WARM: bake the PRT sky-visibility transfer (holographic_prt: shadowed hemisphere -> SH) at a COARSE anchor grid. + * HOT: serve every other pixel by a SMOOTH, normal-aware Gaussian gather of its neighbouring anchors. The smooth + neighbourhood gather (not a 2x2 bilinear) is what fixed the "blocky shadows" Moose spotted -- bilinear on a coarse + grid fits a curved AO ramp with flat facets; the Gaussian overlap has no grid creases. Normal-aware so we never + blend the sky response across a geometry edge (light leak). KEPT NEGATIVE fixed: the first cut used bilinear and + faceted; measured the residual was grid-periodic; the Gaussian gather made it non-periodic (on-grid ~= off-grid). + * COLD: recompute transfer EXACTLY only at the misses -- pixels the interpolation couldn't serve (silhouettes) plus + the sharpest-gradient pixels (the vision.gradient sharpness map is the hit/miss policy). The pathfinding at the + discontinuities, spent only there. + +Measured (showcase scene, 180x120): 15-16x faster than baking every pixel, ~40x faster than a one-pass path-traced +dome, at 0.004 mean error vs the full bake and NOISE-FREE (grain 0.0001 vs 0.019 brute), ~96% cache hit rate. On a +96x96 tour frame: 7.7x vs full bake. The 323s showcase dome is now sub-second. + +Wired: render_scene_document(..., dome_cache=False) default-off -- when True, DomeLights are pulled from the +per-sample lights and served by the screen-space cache, added back; byte-identical when off (pinned). Threaded +through UnifiedMind.render_scene_document. Integration test renders a dome scene through the mind both ways and +checks they agree (|Δmean| < 0.06). Reuses holographic_prt (bake/shade), holographic_vision (Sobel sharpness map), +holographic_raymarch. Tests +6 (lights+hit-rate / shadowed-not-flat / matches-full-bake / no-grid-facets / +empty-gbuffer / mind-pipeline integration). KEPT SCOPE: this is the DOME (softest case, biggest cache win); sharp +and glossy terms stay on the tracer -- the soft/sharp router (measured across the design thread) picks which. + +## Render build, batch 14: MODULATE/DEMODULATE (M1) + DEMODULATED DENOISE (M4) [+7 tests] + +From Moose's 3D-quality backlog. Probed first (don't redo): the five bakes it names -- matcompile, matbake, viewlut, +prt, radiance -- already exist; svgf/atrous_bilateral uses albedo only as an edge-STOP guide, not demodulation; no +demodulate primitive existed. So M1 (the primitive) and M4 (demodulated denoise) are genuine BUILDs. + +M1 -- holographic_modulate.py: demodulate(signal, carrier) = signal/(carrier+eps) [UNBIND], remodulate(residual, +carrier) = residual*carrier [BIND]. A diffuse pixel is a product albedo*irradiance -- crisp carrier times smooth +residual -- so splitting/recombining IS unbind/bind. Names the move the engine already spends as bake-and-query five +times (and the cached dome is another instance): bake the smooth factor, multiply the crisp one at query. Round-trip +exact where the carrier is known. + +M4 -- denoise_demodulated(): divide the albedo out, denoise the smooth IRRADIANCE with the shipped a-trous bilateral +(geometry edge-stops kept: normal+depth; albedo guide uniform), multiply the albedo back. The filter can smooth the +noise hard because there's no texture to smear. MEASURED on a textured+lit noisy synthetic: 33% less error than +filtering colour directly (0.0049 vs 0.0074) while keeping texture edges (0.50 vs true 0.52); tile noise halved. + +KEPT NEGATIVES (measured, loud): (1) demodulation only pays when albedo VARIES -- on a uniform-albedo matte scene +there is nothing to separate, so M4 is ~neutral (within ~5% of guide-only). It is therefore NOT the fix for the +matte-showcase placed-light speckle, which is soft-light SAMPLING noise (needs the dome-style cache extended to the +area lights, a separate item). (2) The carrier must be non-zero: near-black background (sky albedo ~0) explodes under +division, so we mask it (carrier_floor -> carrier 1 there, a plain guide denoise). (3) Diffuse only -- radiance = +albedo*irradiance is a plain product for diffuse; keep guide-only for glossy/view-dependent. + +Wired: render_auto(..., demodulate=False) default-off/byte-identical -> render_scene_document -> UnifiedMind, all +threaded. Tests +7 (round-trip / elementwise unbind / cleaner-on-textured / preserves-edges / masks-black-background +/ neutral-on-uniform / mind-pipeline integration). Reuses holographic_svgf (a-trous). This is the M-thread primitive; +the cached dome (batch 13) is the same bake-and-query move on light. + +### batch 14 addendum -- M5 (super-res demodulation) [+4 tests] + +Continued the M-thread. M5 -- holographic_modulate.superres_demodulated + render_demodulated_upscale: render the +expensive LIGHTING at low resolution, DEMODULATE (divide out the albedo) to the smooth irradiance, upscale THAT +(smooth -> no aliasing), REMODULATE with the CRISP high-res albedo (cheap: material lookup, no light transport). +High-res detail at low-res lighting cost. Measured on a TEXTURED scene (200x200 from a 100x100 render): 2.6x faster +than a full high-res render AND 18% cleaner than a plain colour upscale (err 0.0175 vs 0.0215). + +KEY FIX (measured): demodulate by the high albedo DOWNSAMPLED to low-res (area-average -> anti-aliased carrier), +NOT a point-sampled low-res albedo. The first cut used the point-sampled low albedo and lost to plain upscale (-19%) +because high-frequency texture aliased the carrier and rode through; the anti-aliased carrier matches what the low +render integrated and flips it to +18%. Kept negatives (same as M4): helps where albedo VARIES (texture), neutral on +flat; near-black background masked; diffuse only. Wired as UnifiedMind.render_demodulated_upscale + integration test. +Reuses holographic_fsr (easu_upscale). Tests +4 (beats-plain-on-texture / carrier-from-downsampled-albedo / +neutral-on-uniform / mind-integration). + +## Render build, batch 15: CACHED SOFT AREA LIGHTS (RENDER-DC2) + the .soft-flag bug fix [+7 tests] + +Two things, chasing the placed-light speckle Moose kept showing. + +ROOT-CAUSE BUG FIX (holographic_lights): direct_lighting multi-samples a light only when getattr(light,'soft',False). +The area lights (Rect/Disk/Sphere/Mesh) NEVER set that flag, so every area light was sampled with ONE shadow ray -- a +biased HARD shadow, speckly across the penumbra, and area_samples did nothing. Added self.soft=True to all four area +classes. This alone makes area lights multi-sample (default 2). Exposed a vacuous test (test_area_light_multisampling +_reduces_variance compared two identical 1-sample arrays); fixed it to compare two MULTI-sample counts (1 sample is +degenerate -- always the light centre, zero variance because deterministically blocked). + +CACHED SOFT LIGHTS (holographic_lightcache): the soft-shadowed irradiance is a smooth field, so cache it like the +dome. Refactored holographic_domecache to extract a GENERIC three-tier engine cached_screen_shade(sdf,hit,P,N,albedo, +bake_fn,...); cached_dome_shade now delegates to it with a PRT bake (behavior-preserving, dome tests still pass). +cached_soft_lights_shade builds an NEE bake (many-sampled soft shadow) and calls the same engine. Measured: the cached +soft light is NOISE-FREE (seed-diff 0.0045 at area_samples=48 vs 0.039 for the broken 1-sample path, ~8x cleaner), +96% hit rate. Wired render_scene_document(soft_light_cache=False) + the mind, default-off/byte-identical; splits area +lights out of per-sample NEE, serves them cached, adds back. + +HONEST DIAGNOSIS (measured, loud): on the showcase the soft-light cache removes only ~6% of the total speckle and is +1.7x faster. Isolating direct vs indirect: seed-diff is 0.010 DIRECT-only (max_bounce=1) but 0.038 WITH GI bounces +(max_bounce=3) -- so ~73% of the speckle is INDIRECT / global-illumination bounce noise, which a direct-light cache +cannot touch. The cache correctly cleans the direct soft term (0.0103->0.0081 at bounce=1, 21%); the dominant fix is +still owed. NEXT: cache the INDIRECT bounce irradiance the same way (it's also a smooth field over diffuse surfaces), +which is the real remaining speckle fix. Tests +7 (split / flags / lights+hitrate / noise-free / more-samples-cleaner +/ empty / mind-integration). + +## Render build, batch 16: CACHED INDIRECT / GLOBAL ILLUMINATION (RENDER-DC3) -- the real speckle fix [+3 tests] + +Batch 15's honest diagnosis said the DOMINANT placed-light speckle is INDIRECT/GI bounce noise (~73%), not direct +soft shadows. This caches it. Indirect light varies smoothly over diffuse surfaces (Ward 1988's irradiance-caching +insight), so it fits the same three-tier engine (cached_screen_shade). cached_indirect_shade (in holographic_lightcache): +at each coarse anchor, sample many cosine-weighted hemisphere directions, trace each, and gather the DIRECT light the +bounce surface re-radiates -- using the ACTUAL scene lights + materials (direct_lighting + material_fn), not the +stand-in single-light gather holographic_globalillum.gather_indirect uses. Reuses _cosine_hemisphere from globalillum, +sphere_trace/sdf_normal from raymarch. + +Wired render_scene_document(indirect_cache=False): when True the tracer renders DIRECT-only (max_bounce=1) and the +clean cached one-bounce GI is added -- replacing the tracer's noisy multi-bounce GI. Threaded through the mind. + +MEASURED on the showcase (180x120): the cached GI is NOISE-FREE (seed-diff 0.0062, 96% hit rate) and carries real +colour bleeding (red wall -> floor). PRODUCTION scenario (bake the caches ONCE, then render): placed-light speckle +0.0337 -> 0.0044 = 87% LESS, and ~8x faster (bake 2.8s once + 6.4s/frame direct-only vs 75s/frame multi-bounce). +Single-frame floor grain 47% lower. KEPT NEGATIVES (loud): it's ONE bounce, not full multi-bounce GI, so ~5% dimmer +than the tracer (misses higher-order bounces); diffuse gather (no glossy interreflection); the small remaining noise +is the direct-only tracer's AA-edge jitter, not GI. This is the third and load-bearing piece of the two-mode cached +lighting: dome (DC1) + soft area lights (DC2) + indirect GI (DC3), all on one shared cache engine. Tests +3 +(indirect lights+noise-free / colour-bleeding / mind-integration). + +## Consolidation build, C1: THE CAPABILITY CATALOG -- "search before you build" [+9 tests] + +First item of the consolidation backlog (Phase 0 -- "cheap, stops the bleeding"). Probed first (the backlog's own +rule): holographic_query.capability_registry ALREADY exists (builds a SQL-queryable table of the mind's faculties, +used by holographic_query_programs) -- so C1 is mostly PROMOTE. Built holographic_catalog.py as the richer HOME: + * Capability = {name, does (plain English), example (copy-paste), native (True=stays in the batched vector domain, + False=hops to Python), aliases}. + * Catalog.register_capability / find_capability(problem, k) -- find_capability is a small READABLE token-overlap + match over each entry's name+does+aliases (stop-words dropped, name-hits weighted, deterministic tie-break by + name). No training, NumPy-free. + * default_catalog() seeds the consolidation HOMES the audits named -- the search indices, the caches/bakes, the + field types, distribute/fuse, the kernel verbs -- so they're findable today. + * seed_from_mind(catalog, mind) reuses capability_registry's faculty-walk to auto-register every public mind + method (docstring as `does`), so BOTH curated homes and live faculties are findable (702 total on the mind). + +Wired UnifiedMind.find_capability / register_capability / _capability_catalog (lazily built + cached). Measured: +'search a big pile of vectors' -> Index home; 'my placed light has speckle noise' -> holographic_lightcache (last +batch's work, now discoverable); 'render a scene document' -> the mind's own render_scene_document faculty. This is +the substrate for the whole consolidation effort: as each home (Index/Cache/Field/...) lands, register it here so +"take full advantage in all scenarios" is real. KEPT HONEST: the match is lexical token-overlap, not semantic VSA +similarity (good enough for discovery; could be upgraded to encode `does` with the text encoder). Tests +9 (register/ +find, headline query, native flag, seeded homes, deterministic ranking, no-match, tokeniser, seed_from_mind, mind +integration). + +## Consolidation build, R1: PIPELINE render-strategy dispatch -- the one entry point [+5 tests] + +Second consolidation item (Phase 2, "highest leverage, do first"). PROMOTE of holographic_pipeline (already the +staged entry point with Stage needs/produces + toposort). The render stage hard-wired the pathtrace path +(converge_samples). R1 turns the render methods into a ROUTING (not a merge): a RENDER_STRATEGIES registry -- +name -> (needs, run, does) -- with four strategies that each DELEGATE to their real module and declare what they +need: + * pathtrace -> converge_samples + declfirefly (the exact pre-R1 path; byte-identical, pinned by a test) + * raymarch -> sphere_trace + matte lambert (a fast, noise-free primary-visibility preview) + * prt -> precompute_transfer + shade_prt (relight the G-buffer under an environment SH; needs light_sh) + * radiance -> reconstruct_view (render = a lookup into a baked radiance field; needs radiance_field) +Added RenderSpec.method ("auto"|...) + light_sh + radiance_field (additive). dispatch_render(ctx) resolves the +method (via _pick_render_method for "auto"), CHECKS the chosen strategy's needs are present, and raises a clear +PipelineError naming the missing field BEFORE running -- the "catch a missing input" the backlog asked for. +_render_run's RenderSpec branch now just calls dispatch_render; _pipe_gbuffer reuses domecache._primary_gbuffer. + +'auto' picks pathtrace whenever a material is present, so every existing RenderSpec caller is byte-identical (all +27 prior pipeline tests pass unchanged). Registered "Pipeline (render/sim)" in the C1 catalog. Verified: a render +goes through the Pipeline (auto->pathtrace), each strategy dispatches, and method='prt' without a light_sh raises +"render strategy 'prt' needs (scene, camera, light_sh), but the RenderSpec is missing: light_sh" -- both directly +and through a full build_pipeline run. KEPT HONEST: raymarch is a preview (no GI/lights); prt/radiance need their +baked inputs. Tests +5 (through-pipeline, strategies dispatch, needs-check, byte-identical pathtrace pin, R1<->C1 +integration). + +## Consolidation build, R2: THE FIELD HOME -- one .sample interface over the spatial field backends [+6 tests] + +Third consolidation item. Probed first (twice rewarded): holographic_ndfield is the sparse-reconstruct PATTERN, and +holographic_field.Field already exists but is the UNIT-SPHERE compositional field (it normalises points) -- a +different abstraction. So R2 is a small NEW home for the SPATIAL field reps the render/sim stack uses. Built +holographic_fieldhome.py: class Field with field.sample(points) over R^D, a thin ADAPTER that ROUTES to each +representation's own sampler (route, don't rewrite): + * Field.callable(fn) -> an analytic / oracle field (SDF expression, FPE surface read) + * Field.grid(arr, lo, hi) -> a DENSE grid (holographic_fields.sample_field/_3d), world->index mapping only + * Field.sparse(SparseField) -> the NARROW-BAND field (delegates to SparseField.sample) +field_backends() lists the route targets. DONE-WHEN (met, exceeded): two backends reachable with identical values -- +the dense grid and the callable oracle, sampled through the same Field, agree to 0.0 at grid nodes; the sparse +backend also routes and matches the true SDF within its band. Updated the C1 catalog's "Field" entry to point at the +new home. KEPT HONEST: the sparse backend is narrow-band (matches only inside the band, sign*band outside, by design); +spectral/FPE/region/dirty are named as future backends (spectral evolves state rather than point-sampling, so it +wraps differently). NOT holographic_field.Field (unit-sphere) -- documented to avoid the collision. Tests +6 (dense== +callable at nodes, uniform interface, sparse routing, backends listed, repr/kind, R2<->C1 integration). + +## Consolidation build, H1: THE INDEX HOME -- one nearest-neighbour interface (exact/forest + calibrated abstain) [+7 tests] + +Fourth consolidation item. Before building, used a new AST scout (tools/consolidation_scout.py) to MAP the search +surface across the whole tree (modules, tests, tour, experiments) -- it and a targeted grep found index APIs the +backlog had NOT named: holographic_spatial.knn (Euclidean point k-NN), holographic_uri.nearest (prefix-scoped), +holographic_octree.query, holographic_lexicon/encoders.nearest -- alongside the named tree(HoloForest)/pivot/rayindex. + +Built holographic_index.py: class Index over a set of vectors, Index.nearest(query, k, abstain=alpha) -> +[(label_or_index, score), ...] best first. ROUTES by size (route, don't rewrite): EXACT cosine scan for small sets +(reuses holographic_ai.nearest verbatim for the top-1 core), the sub-linear RP-FOREST (HoloForest, built with the +correct HoloForest(dim,...).build(items) API) for large; 'auto' picks by forest_threshold. Optional calibrated +ABSTAIN via holographic_honesty.RecallNull -- return [] when the best hit is noise-level. Deterministic tie-break +(descending score, ascending index via lexsort). + +HONEST SCOPE (kept loud): this is the COSINE/vector NN family only. holographic_spatial.knn (Euclidean over point +clouds) and holographic_rayindex (which pixels a ray touches) are DIFFERENT metrics/purposes and stay their own +homes -- registered in the catalog so they're findable, NOT force-merged (the "don't over-consolidate" rule). For a +forest-backed index, k>1 and abstain fall back to an exact rank over the full set (documented; sub-linear path is +top-1). + +Wired UnifiedMind.build_index; updated the C1 catalog "Index" entry + added spatial.knn / rayindex entries. +DONE-WHEN (met): TWO delegates route through Index -- holographic_lexicon.nearest and holographic_encoders. +TextEncoder.nearest -- both with BYTE-IDENTICAL rankings vs their old cosine loops (verified), and the recall +benchmark (bench_recall.py, uses HoloForest/brute-force directly) is UNCHANGED (forest @1 100% to 8k, 97% at 20k, +sub-linear comparisons). Tests +7 (exact==forest, top-k ordered/deterministic, labels, abstain, auto-routing, empty, +H1<->C1<->delegates integration). Also shipped tools/consolidation_scout.py (AST dupe-finder + concept-locator). + +## Consolidation build, H2: THE CACHE HOME -- bake-and-query, one shared grid core over the scattered bakes [+8 tests] + +Fifth consolidation item. Scouted the bake/cache surface first (tools/consolidation_scout.py concept cache + grep): +the real bakes are matbake.bake_field / bake_material, sdfbake.bake_sdf_grid, viewlut.bake_view_lut, anim. +bake_deformation, prt.precompute_transfer. The two POSITION bakes (matbake, sdfbake) rewrite the IDENTICAL grid +generation -- np.linspace -> meshgrid(indexing='ij') -> stack -- then their own trilinear reader. + +Built holographic_cachehome.py: class Cache (a namespace of staticmethods). The load-bearing shared core is +Cache.grid_points(lo, hi, res) -> (points, res3): the ONE position-grid generator both bakes duplicated. Plus +Cache.bake_grid(evaluator, lo, hi, res) (sample + reshape), Cache.bake(evaluator, vary=...) dispatching by what +VARIES (constant=compute once; position=grid+BakedGrid trilinear lookup; view->viewlut.bake_view_lut; +time->anim.bake_deformation), and a BakedGrid reader mirroring matbake.BakedField. + +DONE-WHEN (met): matbake.bake_field AND sdfbake.bake_sdf_grid now call Cache.grid_points, producing BIT-IDENTICAL +grids to their old inline meshgrid (verified: matbake scalar grid, sdfbake dist+ids grids all np.array_equal to the +reference; their existing tests pass). Route, don't rewrite: each bake keeps its own lookup reader (BakedField / +GridSDF); only the precompute is shared. Wired UnifiedMind.bake; updated the C1 catalog Cache entry. KEPT HONEST: +NOT holographic_cache.py (Ward irradiance-GRADIENT cache -- sparse anchors + Jacobian, a different scheme), +documented to avoid the collision. Tests +8 (grid matches inline, per-axis res, scalar/channel bake, trilinear-exact +at nodes, constant strategy, matbake+sdfbake bit-identical delegation, backends). + +## Consolidation build, R3: MATERIAL + SHADING -- the three-way (Material -> Cache -> Shading) [+2 tests] + +Sixth consolidation item. Scouted the BRDF/shading surface (scout concept + a grep hunt for inline Schlick-Fresnel/ +GGX): the SPECULAR model is already centralised in holographic_brdf (cook_torrance, sample_brdf, fresnel_dielectric) +and the real render paths CALL it (pathtrace imports sample_brdf/fresnel_dielectric; lights.direct_lighting uses +cook_torrance) -- NO re-derived Fresnel/GGX found. So the "render methods call Shading, not re-derive" half of R3 was +already true (another probe-first win). + +The one genuine remaining de-dup: the DIFFUSE lambert term (clip(N.L,0)*albedo) was re-derived inline in the gather +paths, and brdf had no standalone lambert (it lived only inside cook_torrance). Added holographic_brdf.lambert(N, L, +base_color) -- the diffuse half exposed on its own -- and routed holographic_globalillum.gather_indirect to it, +BIT-IDENTICAL (verified). KEPT HONEST: the other lambert-looking sites (raymarch, surface, checkerboard) are COMPOUND +shades that fold in shadow / occlusion / a Phong spec / an ambient blend -- not pure lambert -- so they keep their own +expression (don't over-consolidate); lambert is only the diffuse term. + +DONE-WHEN (met): the three-way composes end to end -- a SurfaceMaterial's position channels BAKE via the Cache home +(matbake.bake_material -> bake_field -> Cache.grid_points, H2), a surface pulls its channels from that baked Material, +and it SHADES via the Shading home (brdf.cook_torrance + lambert). Registered "Material (channels)" and "Shading +(BRDF)" in the C1 catalog. Tests +2 (lambert bit-identical/back-facing/shape; R3 three-way integration). + +## Consolidation build, R4: THE SAMPLING HOME -- patterns / directions / MIS / accumulation in one place [+8 tests] + +Seventh consolidation item (thin). Scouted the sampling surface (scout concept + grep): the pieces are shipped but +scattered -- lowdiscrepancy (quasi-random), sampling (Poisson/blue-noise), mis (Veach), accumulate (firefly clamp) -- +and the COSINE-HEMISPHERE direction sampler was re-implemented in THREE modules (brdf._cosine_sample, +globalillum._cosine_hemisphere, lights._cosine_hemisphere). + +Built holographic_samplinghome.py: class Sampling (staticmethods). THIN -- routes to the shipped modules for +low_discrepancy / poisson_disk / mis_weight / accumulate (route, don't rewrite); the one thing it OWNS is the +vectorised cosine_hemisphere(N, n, seed), promoted here (the exact globalillum code) so the copies collapse to one. + +Routed FIVE call sites through it, all bit-identical: pathtrace's AA offsets (Sampling.low_discrepancy -- the R4 +done-when), globalillum._cosine_hemisphere (now a delegate) + lightcache's gather import, and the mind's +low_discrepancy_sample + blue_noise_sample faculties. Registered "Sampling" in the C1 catalog. KEPT HONEST: the +rng-based single-dir samplers (brdf._cosine_sample, lights._cosine_hemisphere) have different signatures (rng vs seed, +1 vs n dirs) so they were left as-is rather than force-fit -- only the vectorised seed-based one was shared. Tests +8 +(low-discrepancy routes exactly, hemisphere unit/upper/deterministic, hemisphere==globalillum bit-identical, firefly +clamp, poisson routes, backends; R4 integration through pathtrace + mind). + +## Consolidation: DISCOVERABILITY SWEEP -- make the whole engine reachable (no buried functionality) [+5 tests] + +Moose's point: the engine has many pipelines/domains (adaptive rendering, sampling, lights, materials, textures, +fields, geometry, caches, simulation, physical/chemical properties, ...) and "there's no point in unreachable or +buried functionality." The catalog (C1) only curated ~24 homes + auto-seeded mind faculties, so most of the 344 +modules could not be FOUND by describing a problem. + +Fixed at the root: added holographic_catalog.seed_from_modules(catalog) -- AST-reads EVERY holographic_*.py module's +docstring (never imports; the docgen discipline) and registers it as a findable capability with its one-line summary +as `does`, tagged by domain family (_family_of + _FAMILY_KEYWORDS). Wired UnifiedMind._capability_catalog to seed +from modules too, so find_capability now covers homes + faculties + every module (700+ entries on the mind, 362 in +the bare module catalog). Added top-level DOMAIN homes to default_catalog: Lighting, Geometry, Texture, Simulation, +Physics & chemistry, Adaptive rendering, Denoise, Query/database -- one findable pointer per subsystem. + +Built tools/reachability_audit.py (AST-only): reports, per module, docstring present? public API? referenced by the +mind? recorded negative? It FOUND the one genuinely buried module -- holographic_sdfscene had its documentation in +`#` comments, not a docstring, so AST couldn't read it -> undiscoverable. Converted its header to a real module +docstring. Audit now: 303 modules reachable via a faculty, 7 deliberately-unwired recorded negatives, 163 modules +that DOCUMENT a kept negative (honest measurement, good), NO-DOCSTRING = 0. Added a GUARD test +(test_every_engine_module_has_a_docstring) so no future module can become buried, plus domain-coverage tests and a +through-the-mind integration test. Verified: 'thin film iridescence' -> holographic_thinfilm, 'MPM granular' -> +holographic_mpm, 'hair grooming' -> holographic_groom, 'a scene as SDF parts' -> holographic_sdfscene (the fixed +one) -- all reachable by plain description. Fixed a stale example (Physics home referenced a non-existent +holographic_matter; the model is in holographic_mixture). Tests +5. Also shipped tools/reachability_audit.py. + +## Consolidation build, R5: THE DENOISE HOME -- image / sharpen / signal, one home [+9 tests] + +Eighth consolidation item (thin). Scouted the denoise surface: it was scattered across svgf (a-trous bilateral for +images), modulate (demodulated denoise), sharpen (Van-Cittert), diffuse (denoise-as-diffusion), and the manifold +family in denoise/hopfield. The SIGNAL/vector denoisers already have a home -- UnifiedMind.denoise dispatches +manifold/adaptive/codebook/nlm/trajectory/spectral/pnp -- so that half was done (probe-first). The scattered part was +IMAGE denoise: the pipeline stage, mind.svgf_denoise, and modulate all called svgf/modulate directly. + +Built holographic_denoisehome.py: class Denoise (staticmethods) split by what you clean -- + * Denoise.image(img, normal, albedo, depth, method='svgf'|'demodulated', ...) -> svgf.atrous_bilateral / modulate. + denoise_demodulated. + * Denoise.sharpen(x, ...) -> sharpen.sharpen_loop. + * Denoise.signal(x, samples/codebook/method='auto') -> holographic_denoise (manifold/adaptive/codebook/trajectory) + -- the lighter library entry over the same primitives UnifiedMind.denoise dispatches (the Milanfar reframe: + cleanup/consolidation ARE denoisers). +All thin routing (route, don't rewrite). + +DONE-WHEN (met): the PIPELINE's _svgf_run denoise stage now calls Denoise.image(method='svgf') -- bit-identical to +the old atrous_bilateral call. Also routed mind.svgf_denoise -> Denoise.image and mind.sharpen_loop -> Denoise.sharpen, +both bit-identical (verified). Updated the C1 catalog Denoise entry to point at the home. KEPT HONEST: the signal +family stays on UnifiedMind.denoise (fuller: adds nlm/pnp/spectral + auto sigma); Denoise.signal is the lighter entry, +not a competing dispatcher. Tests +9 (image svgf cleans + bit-identical, demodulated routes, unknown-method raises, +signal trajectory/manifold clean & route, auto + missing-prior, sharpen routes, backends; R5 pipeline+mind integration). + +## Consolidation build, R6: THE TEXTURE HOME -- detail fields feeding Material channels [+7 tests] + +Ninth consolidation item (thin) -- completes the thin trio. Scouted the texture surface: detail is generated in +holographic_noise (FractalNoise fBm), holographic_curlnoise (divergence-free curl), holographic_cellular +(VoronoiCells), holographic_texturesynth (patch synthesis), plus the weathering set (oxidation/burn/inclusions). +They all feed the SAME consumer -- a Material channel's Param(field=callable) or Param(map=grid) (from R3). + +Built holographic_texturehome.py: class Texture (staticmethods) returning channel-ready detail -- + * Texture.fbm(...) -> a fractal-noise field callable (wraps FractalNoise; query is per-point so the field loops, + fine for a channel baked once via the Cache home). + * Texture.voronoi(...) -> a VECTORISED cellular field: kind='edge' (crack/grout distance) or 'id' (per-cell tint). + * Texture.curl(...) -> a divergence-free (u,v) flow grid for warping. + * Texture.synth(sample, ...) -> a larger texture image grown from a small sample (Param(map=...)). +Thin routing (route, don't rewrite); the generators stay their own modules, the home is only the "return something a +channel can eat" adapter. + +DONE-WHEN (met): a Material channel is SOURCED through Texture -- a Voronoi crack field drives a SurfaceMaterial +roughness channel, verified end to end. Better, it closes the chain across the earlier homes: Texture -> Material +(R3) -> Cache (H2, the channel bakes via Cache.grid_points) -> Shading (R3, cook_torrance) -- one integration test +walks all four. Updated the C1 catalog Texture entry to point at the home. KEPT HONEST: FractalNoise.query is +per-point (not vectorised) so Texture.fbm loops -- acceptable for a baked channel, noted. Tests +7 (voronoi vectorised/ +deterministic, channel-sourced-through-texture, fbm field, curl divergence-free, backends; R6 Texture->Material->Cache +->Shading integration). + +## Consolidation build, R7: THE LIGHTING HOME -- light types + the shade integral, one door [+6 tests] + +Tenth consolidation item ([BUILD], not thin). Scouted the lighting surface: the light TYPES (point/directional/spot/ +area/dome/IES) live in holographic_lights, but the shade INTEGRAL is reached for directly in several render paths -- +lights.direct_lighting (NEE over placed lights), prt.shade_prt (baked relight under SH), domecache.dome_light_sh +(dome->SH), gather_indirect (one-bounce). No single lighting entry point. + +Built holographic_lightinghome.py: it RE-EXPORTS the 10 light types (so `from holographic_lightinghome import +Lighting, RectLight` is the one lighting import) and class Lighting exposes the shade integral in each mode: + * Lighting.direct(sdf, P, N, V, albedo, metallic, roughness, lights, rng, ...) -> NEE from placed lights + shadow + rays (routes to holographic_lights.direct_lighting). + * Lighting.prt(transfer, light_sh, albedo) -> relight a baked transfer under an environment SH (routes to prt). + * Lighting.environment_sh(dome, order, n) -> project a dome/sky to SH (routes to domecache). + * Lighting.split_cached(lights) -> partition into dome / soft-area / hard, so a render method routes its lights + without knowing the flags. +Route, don't rewrite: the Cook-Torrance-per-light integral stays in holographic_lights, PRT in holographic_prt; only +the entry point is unified. + +DONE-WHEN (met, exceeded): TWO+ render methods now get their lighting from Lighting -- the cached soft-light pass and +the cached indirect pass (holographic_lightcache -> Lighting.direct) AND the pipeline's PRT strategy +(holographic_pipeline._strat_prt -> Lighting.prt). All bit-identical (Lighting.direct == direct_lighting verified; +lightcache deterministic; PRT strategy renders finite). Updated the C1 catalog Lighting entry to point at the home. +Tests +6 (direct integral bit-identical, light types re-exported, split_cached partitions, prt relight, modes; R7 +two-render-method integration). + +## Consolidation build, R8: THE SHADOW / VISIBILITY HOME -- visibility strategies, one door [+6 tests] + +Eleventh consolidation item ([BUILD], R7's companion). Scouted the visibility surface: "can light reach this point?" +is spelled four ways -- SDF SOFT shadow (Quilez penumbra) + SDF AMBIENT OCCLUSION in holographic_raymarch (re-imported +by holographic_raycoherence and holographic_semantic), the HARD shadow-RAY test embedded in +holographic_lights.direct_lighting, and PRT's baked visibility (holographic_prt). (Note: holographic_occlusion is a +DIFFERENT concept -- VSA recall under occlusion, not rendering visibility -- and was correctly left alone.) + +Built holographic_shadowhome.py: class Shadow (staticmethods), each spelling a named strategy -- + * Shadow.soft(sdf, P, Ldir, ...) -> SDF soft shadow [0,1] (routes to raymarch.soft_shadow). + * Shadow.ambient_occlusion(sdf, P, N, ...) -> SDF AO [0,1] (routes to raymarch.ambient_occlusion). + * Shadow.hard(sdf, P, N, light_dir, dist, ...) -> shadow-RAY visibility 1/0, the NEE test EXTRACTED so any render + path can call it (sphere_trace: visible = ~hit | t>dist-eps). + * Shadow.prt_visibility_note() -> documents the baked (fourth) strategy without importing prt eagerly. +Route, don't rewrite: the Quilez marches stay in raymarch; PRT visibility stays baked in prt. + +DONE-WHEN (met): TWO render paths now get their visibility from Shadow -- holographic_raycoherence (1 soft + 1 AO +call) and holographic_semantic (4 soft + 4 AO calls) were switched from importing raymarch's shadow fns to calling +Shadow.soft / Shadow.ambient_occlusion, BIT-IDENTICAL (their 25 tests pass). Registered "Shadow / visibility" in the +C1 catalog. KEPT HONEST: the SDF soft/AO/hard-ray need the point offset off the surface (P + N*eps) as the callers do +-- documented. Tests +6 (soft darker under occluder + bit-identical, AO in-range + bit-identical, hard-ray blocks/ +clears, strategies, prt note; R8 two-render-path integration). + +## Consolidation promote, H3: THE SCALE HOME -- partition + monoid-reduce, one door [+9 tests] + +Twelfth consolidation item ([PROMOTE]). Scouted the scale-out surface: the machinery already ships in +holographic_distribute -- partition / adaptive_partition (load-balanced), partition_2d / partition_3d (image tiles / +volume bricks), distribute_bricks (place a worker per region), distribute (the map-reduce), and the commutative-monoid +reducers (reduce_sum/min/max/bundle/sum_exact). The mind already wired it as faculties (distribute_compute, +partition_domain, partition_grid, distribute_bricks). What was MISSING was the plain library HOME the other homes have. + +Built holographic_scalehome.py: class Scale (staticmethods) promoting holographic_distribute -- + * Scale.map_reduce(buckets, worker, reduce='sum'|'min'|'max'|'bundle'|'exact'|callable, cache=None) -> the core + partition + commutative-monoid reduce (routes to distribute; the reducer table moved here). + * Scale.partition(n, k, costs=None) -> load-balanced buckets. Scale.tiles(shape, blocks) -> 2D tiles / 3D bricks. + Scale.bricks(out_shape, regions, worker, ..., skip) -> place-per-region, skip empty bricks. + * Names the five STRATEGIES: tiling (here), octree (prune empty space), multires (coarse-to-fine), superposed (VSA + bundling -- scale by superposition not storage), sparsefield (store only active cells). + +DONE-WHEN (met): the mind's four scale faculties (distribute_compute / partition_domain / partition_grid / +distribute_bricks) now DELEGATE to the Scale home, all bit-identical -- and the map-reduce RESULT MATCHES the un-split +computation (map_reduce of a 1000-vector == np.sum, verified). The reducer table (sum/min/max/bundle/exact) now lives +in the home, so distribute_compute is a thin delegate. Registered "Scale (distribute)" in the C1 catalog. Tests +9 +(map_reduce matches un-split sum + bit-identical, partition load-balances, tiles cover 2D/3D disjointly, min/bundle +monoids, bricks place+skip, backends/strategies; H3 mind-scaler integration). + +## Consolidation promote, H4: THE BLEND HOME -- combine into one, one door [+8 tests] + +Thirteenth consolidation item ([PROMOTE]). Scouted the blend/merge/interpolate surface: "combine these into one" is +spelled many ways -- superposition (holographic_ai.bundle), spherical interp (holographic_ai.slerp), Frechet/Riemannian +mean (holographic_sphere.frechet_mean), front-to-back alpha compositing (occlusion/splat), dict/scene merge -- and a +soft weighted blend normalize(sum w_i v_i) is re-derived in blend skinning (blendpose), the matter model (mixture), etc. + +Built holographic_blendhome.py: class Blend (staticmethods) promoting the canonical ops -- + * Blend.bundle(vectors, weights=None) -> superposition; weighted = normalize(sum w_i v_i) (routes to ai.bundle when + unweighted; the weighted path matches blend_pose exactly). + * Blend.lerp / Blend.slerp (routes to ai.slerp) -> chord vs shortest-arc interpolation. + * Blend.mean(vectors, weights) -> Frechet mean on the sphere (routes to holographic_sphere). + * Blend.alpha_composite(colors, alphas) -> front-to-back OVER compositing. + * Blend.merge(a, b, policy='prefer_a'|'prefer_b'|'average') -> the scene/workspace combine discipline made explicit. + +DONE-WHEN (met): TWO delegates now call Blend, bit-identical -- holographic_blendpose.blend_pose delegates to +Blend.bundle (the weighted bundle), and holographic_generate.morph_images takes its slerp from Blend.slerp. Registered +"Blend (combine)" in the C1 catalog. KEPT DISTINCT ON PURPOSE (don't over-consolidate): phasemorph's PHASE-domain +shortest arc (angle interpolation on unit phasors), mixture's solvent-base weighted DENSITY (a physical blend with a +baseline term), and occlusion's alpha-composited bundle READOUT (a decompression, not a forward blend) each stay in +their own module -- they are not the same operation. Tests +8 (weighted bundle == blend_pose, unweighted == ai.bundle, +slerp == ai.slerp + on-sphere, lerp chord, alpha composite, merge policies, frechet mean; H4 two-delegate integration). + +## Consolidation promote, H5: THE TRANSFORM HOME -- move/rotate/warp, one facade + a real dedup [+8 tests] + +Fourteenth consolidation item ([PROMOTE]). Scouted the transform surface and found a genuine DUPLICATION: the basic +4x4 matrix builders (translation / scaling / compose) were copied between holographic_scenegraph and +holographic_transform (the modeling-app gizmo kit). translation/scaling/compose were BIT-IDENTICAL across the two; +rotation was NOT (scenegraph Rodrigues vs transform quaternion-to-matrix, ~1e-12 apart). + +Built holographic_transformhome.py: class Transform -- one facade over "move/rotate/warp" across representations, +routing to each module (route, don't rewrite): + * VSA: Transform.bind (rigid shift = one binding) / permute (order) -> holographic_ai. + * geometric 4x4: translation/scaling/rotation/compose/decompose/compose_trs/look_at -> holographic_transform. + * rotor: Transform.rotor/rotate_vec (gimbal-lock-free geometric-algebra rotation) -> holographic_clifford. + * anisotropic: Transform.steer_bandwidths -> holographic_steering. + +DONE-WHEN (met) + a real de-silo: TWO delegates route through Transform -- holographic_scenegraph's +translation/scaling/compose_transforms now DELEGATE to the home (deduping the copied matrix math, bit-identical), and +holographic_procgen builds its instance transforms through Transform.translation/scaling. KEPT DISTINCT ON PURPOSE: +scenegraph's Rodrigues rotation is NOT routed (it's ~1e-12 off the quaternion rotation, and flipping those bits could +flip a downstream tie -- the bind_batch lesson), and splatexport's 3DGS-ordered quaternions + cosserat's quaternion +helpers stay put (different conventions). Registered "Transform (warp)" in the C1 catalog. Tests +8 (matrices route +bit-identical, translate moves a point, TRS round-trip, bind/permute route, clifford rotor x->y, scenegraph-rotation- +kept-distinct, kinds; H5 two-delegate dedup integration). + +## Consolidation promote, H6: THE MEMORY HOME -- cache-hierarchy levers, one door [+7 tests] + +Fifteenth consolidation item ([PROMOTE]). Scouted the memory/cache surface, keeping the RENDERING caches (Ward +gradient cache holographic_cache, baked grids -- H2's Cache home) DISTINCT from the memory-HIERARCHY machinery. The +hierarchy levers already ship: holographic_residency (SpectrumCache LRU of atom->rfft(atom) + bind_cached/unbind_cached, +bit-identical to bind), the batched contiguous bind (holographic_ai.bundle_bind/bind_batch -- one FFT for a whole +record), tiling-to-fit (Scale.tiles), and the opt-in backends (holographic_backend GPU / holographic_jit numba). The +mind already exposed spectrum_cache + fuse_record. What was missing was the single "keep the hot working set close" door. + +Built holographic_memoryhome.py: class Memory over the levers (route, don't rewrite) -- + * Memory.spectrum_cache / bind_cached / unbind_cached -> residency (reuse a resident FFT spectrum; bit-identical). + * Memory.bind_batch(keys, values) -> one batched, contiguous FFT for a record (cache-resident layout). + * Memory.tiles(shape, blocks) -> cut a working set into cache-sized tiles (delegates to the Scale home, H3). + * Memory.backend(kind) / gpu_available() -> select the opt-in GPU/numba path, graceful NumPy fallback. +Documented the L1-L4+RAM hierarchy as the organizing frame. + +DONE-WHEN (met, measured): (1) the residency path is reachable through Memory AND through the mind -- routed +UnifiedMind.spectrum_cache to Memory.spectrum_cache, bit-identical; bind_cached == plain bind verified. (2) the batched +kernel is measurably CACHE-RESIDENT -- encoding a 64-pair record in ONE FFT over stacked contiguous arrays runs ~2-3x +faster than a Python loop of per-pair binds (min-of-rounds timing, robust to load). Registered "Memory (cache +hierarchy)" in the C1 catalog. KEPT NEGATIVE carried: bind_batch's batched FFT is ~1e-12 off the scalar-loop bind, so +it stays OUT of tie-sensitive decision paths. Tests +7 (bind_cached bit-identical + residency reuse, unbind_cached +recovers, batched kernel faster [min-timing], tiles cover, backend fallback, residency-through-mind, levers). + +## Consolidation promote, H7: THE COMPUTE HOME -- stay VSA-native, one door [+6 tests] + +Sixteenth consolidation item ([PROMOTE]) -- the last H-home. Scouted the fuse/schedule/execute surface: the +VSA-native compute levers already ship -- holographic_fuse (collapse a bind/bundle/permute expression tree into ~2 +FFTs, one forward per distinct leaf + one inverse out, with fft_counts() to MEASURE the win), holographic_schedule +(the cost model: fuse runs of linear ops, eager at the boundaries), holographic_superschedule (width), and +holographic_machine (run logic as a VSA program). The mind already had fuse_record/fuse_expression/realize_recipe_fused. +What was missing was the single door and the stated RULE. + +Built holographic_computehome.py: class Compute over the levers (route, don't rewrite) -- + * Compute.leaf/bind/unbind/bundle/sum/permute -> build a fusable expression tree (holographic_fuse builders). + * Compute.fuse(expr, cache) / fuse_record(keys, values, cache) -> collapse into ~2 FFTs (equal to op-by-op to ~1e-15). + * Compute.reset_fft_counts() / fft_counts() -> MEASURE the FFT count. + * Compute.run_recipe / run_scheduled -> the fuse-runs scheduler (holographic_schedule). + * Compute.machine(dim, seed, ...) -> the stored-program VSA machine (holographic_machine). +The rule, stated once: PUSH decisions and cleanups to the BOUNDARIES; keep the linear middle in the vector domain +(every exit to Python costs an FFT round-trip). + +DONE-WHEN (met, measured): a multi-op chain runs FUSED with a measured FFT-count drop -- fusing a 32-pair record took +66 FFTs vs 96 for the op-by-op path (~31% fewer, exactly 2*len+2), and the fused result agrees with the eager one to +FFT tolerance. Routed UnifiedMind.fuse_record + fuse_expression through the Compute home, bit-identical. Registered +"Compute (VSA-native)" in the C1 catalog. KEPT NEGATIVE carried: the fused/batched FFT is ~1e-15 off the op-by-op +result, so tie-sensitive encoders (the maze-rescue path) must NOT use it -- documented on the faculty. Tests +6 +(fewer-FFTs-measured, fuse_record agrees op-by-op, fuse_expression agrees eager, matches mind faculty, machine +reachable, levers). + +## Consolidation build, R9: THE SIMULATION SCAFFOLD -- one step loop, solvers kept separate [+7 tests] + +Seventeenth consolidation item ([BUILD]) -- the last real build. Scouted the solver surface (292 step/advance hits): +the solvers are legitimately DIFFERENT algorithms with DIFFERENT step signatures -- StableFluid.step() (no args), +softbody.step(dt, gravity, iterations, ...), Fire.step(dt, ambient_K, ...), MPMSnow.step(dt), CosseratStrand.settle(), +HyperCA.evolve(steps), etc. The GOLDEN RULE for R9: do NOT merge them (that would destroy the differences that make each +correct). Also found the scaffold already half-exists: holographic_integrate.SimStep + SolverAdapter ("wrap an existing +solver behind a uniform advance") + the integrators. + +Built holographic_simulationhome.py: class Simulation -- a shared STEP LOOP over ANY solver, built on +integrate.SolverAdapter. The solver's math is untouched; the Simulation only (1) gives it one interface step(dt) / +run(steps, dt) via the SolverAdapter (the SimStep the render Pipeline's sim stage already calls), and (2) exposes its +field as a Field (R2) so volume_render / the Pipeline (R1) can draw it (a 2D grid is lifted to a thin slab for the 3D +marcher). Strategy factories adapt a specific solver in a few closures: Simulation.for_fluid(StableFluid), +Simulation.for_automaton(HyperCA); add any other (softbody/mpm/combustion/...) the same way -- its step closure + its +field extractor. + +DONE-WHEN (met): TWO genuinely distinct solvers -- Stable Fluids (advect+project) and a reaction-diffusion automaton -- +step through the SAME loop, and the Pipeline renders BOTH fields (volume_render, alpha ~0.95 / ~1.00). The solvers +stayed SEPARATE (fluid still a StableFluid with fluid.step(); automaton still a HyperCA with ca.evolve(1)) -- only the +interface is shared. Updated the C1 catalog Simulation entry to point at the scaffold. This composes the earlier homes: +Simulation.field() is a Field (R2), rendered through the volume path (R1). Tests +7 (two-solvers-one-loop, pipeline +renders both, solvers-stay-separate, field-is-a-Field, grid-advances, strategies; R9 integration). + +## Consolidation build, D1: THE HYPERVECTOR DATATYPE -- the capstone, one datatype named [+8 tests] + +Eighteenth and FINAL consolidation item ([BUILD], thin). Scouted for an existing wrapper -- there was NONE; the whole +engine had been operating on one datatype implicitly (a high-dimensional numpy array carrying meaning), passed around +bare so its dim / which-encoder-made-it / "what am I" all lived in the caller's head. So this is a genuine build. + +Built holographic_hypervector.py: class Hypervector -- THIN by design. Carries the raw array + dim + encoder + tag. + * MAKE (the encoders are the constructors): Hypervector.wrap(array, ...) or Hypervector.encode(encoder, value) -- + build one from data via any encoder (scalar/text/record/FPE/UniversalEncoder or a plain callable). + * CONSUME (the five VSA verbs as METHODS, each returning a Hypervector, each matching the bare holographic_ai op + exactly): bind / unbind / bundle / permute / cleanup. Verbs accept a Hypervector OR a raw array on the other side, + so the wrapper mixes freely with existing code. + * READ: cosine / decode. + * The raw array is NEVER hidden: .array, .raw() (no copy), and __array__ so np.asarray(hv) returns the raw array + with no copy -- the hot paths never pay for the wrapper, and nothing existing had to change. +Added a mind faculty: UnifiedMind.hypervector(x, modality, tag) -- encode via the one UniversalEncoder and return a +first-class Hypervector (the encoder-as-constructor tie). + +DONE-WHEN (met): build one from any data (m.hypervector(0.3) / Hypervector.encode(enc, v)), call the five verbs as +methods (all bit-identical to the bare ops), and get the raw array back cheaply (.array / np.asarray(hv) with no copy). +cleanup snaps to the nearest atom in a Vocabulary / dict / (N,dim) array. Registered "Hypervector (datatype)" in the C1 +catalog. Tests +8 (encode carries metadata, five verbs match bare ops, verbs accept raw arrays, raw-array-no-copy, +cleanup dict/array/vocab, cosine round-trip, mind faculty, repr; D1 integration through the mind). + +### CONSOLIDATION BACKLOG COMPLETE (18/18) +All items shipped: C1 (catalog) + the R-homes (R1 Pipeline, R2 Field, R3 Material+Shading, R4 Sampling, R5 Denoise, +R6 Texture, R7 Lighting, R8 Shadow, R9 Simulation) + the H-homes (H1 Index, H2 Cache, H3 Scale, H4 Blend, H5 Transform, +H6 Memory, H7 Compute) + D1 (Hypervector datatype), plus the discoverability sweep (catalog seeds from every module; +reachability audit; every-module-has-a-docstring guard). Golden rule held throughout: ROUTE, don't rewrite -- distinct +algorithms kept as strategies, only the scaffolding unified; every routed call pinned bit-identical (or the difference +kept as a loud negative, e.g. scenegraph Rodrigues rotation, bind_batch's ~1e-12). + +## RE-ENABLE audit: shelved methods that adaptive dispatch can bring back [+15 tests] + +Moose's prompt: now that we have a catalog + adaptive dispatch, reconsider functionality we kept OUT because it only +worked in a niche. "Only good in a niche" flips from a reason to SHELVE into a reason to GATE -- run the niche method +only when a cheap, conservative, deterministic detector says we're in its regime; safe default everywhere else. +Probed the audit doc's candidates against the LIVE code (probe-first). Outcomes -- honest, one per candidate: + +BUILT THE PATTERN: holographic_regimegate.RegimeGate(name, detect, threshold, superior, fallback, above) -- .apply(x) +returns (result, info{score, threshold, used}). Conservative by construction: the FALLBACK is the safe default, so a +misfire costs at most the default, never worse than the shelved negative. Deterministic. This is the reusable home for +every re-enable. + +PASS -- closed-form ITERATE (holographic_iterate.iterate_gated, wired as Compute.iterate). For a LINEAR operator that +is a circular convolution (a bind), the iterate is diagonal in Fourier, so step_k jumps k iterations in ONE FFT -- +EXACT (matches stepping to ~1e-8), not an approximation. Detector: recover the impulse response op(delta) and verify +op == bind(kernel, .) on seeded probes (decidable). Because it's exact in regime, the gate NEVER does worse than +stepping -- it matches (bind) or falls back (nonlinear). MEASURED speedup vs stepping: 2.3x @ k=50, 17x @ k=500, 186x +@ k=5000, all exact. Only attempts the closed form when k >= min_k so the detector pays for itself. Clean win, shipped. + +FAIL (kept shelved, measured why) -- PROJECTION DENOISE gated by noise. The audit put it in Group A ("cheap noise +estimate"), but probing showed the detector is NOT robust. The plain fixed-rank projection wins at high noise +(+~10 dB) and HURTS at low noise (over-smooths real off-manifold detail). MEASURED: (1) the built-in +adaptive_manifold_denoise does NOT fix the low-noise harm (slightly worse); (2) a residual-ratio detector +(||x-project(x)||^2 / (sigma_est^2 (D-rank))) looked clean at one detail level but LEAKS HARM across detail levels -- +a threshold sweep found NO fixed threshold that both guarantees "never worse than the no-op" AND keeps a meaningful win +(even at t=1.25, worst harm -0.27 dB, win collapsed to +0.4 dB). Root cause: at low noise with real detail, +"clean+detail" and "noisy" put energy in the same discarded directions -- a single noisy vector can't cheaply tell +them apart (estimate_sigma is fooled by detail). This is the audit's own caveat firing ("some negatives are +fundamental, not regime-shaped"). So denoise_gated ships as an OPT-IN tool (for callers who KNOW their signal is +low-rank), NOT an auto-default. The negative stays kept. + +OUT (constitution) -- SPARSE/CHEBYSHEV eigenbasis at large N. holographic_spectral's own docstring: a sparse +eigensolver "would need scipy, i.e. a second dependency -- out of scope." NumPy-only constraint forbids it. Scratched. + +REMAINING CANDIDATES (Group A parameter-read, not yet done): brdf multi-scatter GGX at high roughness (needs the +Kulla-Conty term BUILT first, then gate on roughness); fhrr/tensor/sbc at high load (gate on pair/factor count); +mixture phase-field on the tension dial. Group B (coarse-residual): nystrom low-rank probe, splat 3DGS refine, volint +marching, pathtrace adaptive AA, raymarch over-relax -- each once the coarse-first pass lands. Each still owes a +measured breakeven before shipping (the denoise result is the cautionary template). Registered "Regime gate +(re-enable)" in the C1 catalog. Tests +15 (RegimeGate scaffold x5, iterate gate x5, denoise routing x1, plus the +existing suites; re-enable integration x1). Discipline held: probe-first, measure the breakeven, keep the fallback, +keep the negative when the gate can't be made honest. + +## RE-ENABLE #2: FHRR-at-high-load, gated by the pair count [+6 tests] + +Second re-enable from the adaptive-dispatch audit (Group A, parameter-read). Binding N role->filler pairs into one +vector is capacity-limited: real-HRR is cheap and near-perfect at low load but its recall falls off as pairs climb; +FHRR (unit phasors) holds recall far better at high load, at ~2x storage + a little compute. FHRR was kept opt-in +because at low load it "changes nothing" and only costs the complex domain. + +PROBED + MEASURED the crossover (fair, same dim) before setting any threshold: recall ties at low load (both ~1.0), +FHRR pulls away past the knee -- dim=512: N=60 -> HRR 0.71 vs FHRR 0.96; N=100 -> 0.40 vs 0.75. The knee scales with +dim: N ~ 0.06-0.08 * dim (dim=256 knee ~20, dim=1024 knee ~64). Set LOAD_KNEE_FRAC=0.08 (biased slightly high so we +stay on the cheap default until FHRR's win is clear). + +KEY DIFFERENCE from the denoise re-enable: there is NO HARM MODE. FHRR recall is always >= real-HRR, so the gate can +never give a WORSE answer -- a borderline misfire costs only a little storage, never correctness. That makes the load +gate safe by construction (the detector is an exact integer, and the downside of over-triggering is benign cost). + +BUILT holographic_loadmemory.AdaptiveRoleFillerMemory(dim, expected_pairs): choose_backend() reads the pair count +against 0.08*dim and picks 'hrr' (real, cheap) or 'fhrr' (phasor, high recall); uniform add/recall hides the backend +(real trace + Vocabulary vs complex trace + PhasorVocabulary). MEASURED at N=90, dim=512: FHRR 66/90 vs real-HRR 41/90 +-- the win captured automatically. Wired mind.adaptive_record(expected_pairs); registered "Adaptive record +(load-gated)" in the C1 catalog. +6 tests (backend choice by load incl. dim-relative, low-load hrr perfect recall, +high-load fhrr beats hrr, uniform interface, mind faculty; re-enable integration). + +RE-ENABLE SCOREBOARD so far: PASS closed-form iterate (exact in regime), PASS FHRR-at-high-load (no harm mode), +FAIL projection denoise (kept shelved -- detector can't separate detail from noise), OUT sparse eigenbasis (needs +scipy). Remaining Group A: brdf multi-scatter (needs the Kulla-Conty term built first), tensor/sbc exact-recall knees, +mixture phase-field on the tension dial. Group B (coarse-residual): nystrom, splat 3DGS, volint, adaptive AA, +raymarch over-relax. + +## RE-ENABLE #3: tensor-product binding for EXACT recall, gated by fidelity need + memory budget [+4 tests] + +Third re-enable (Group A). Extended the load-gated memory to a THIRD tier: tensor-product (outer-product) binding. +MEASURED (fair, dim=256): tensor stays EXACT (1.00 recall) up to M~D where HRR/FHRR have collapsed -- N=250 gave HRR +0.05, FHRR 0.10, tensor 1.00. The cost is honest and large: D*D numbers = D-times the storage of HRR/FHRR (65536 vs +256 at dim=256). + +So tensor is a no-harm-mode win ON RECALL (exact in-regime) but with a real STORAGE cost, which is why its gate needs +one more decider than FHRR: choose_backend(expected_pairs, dim, exact, max_numbers) picks tensor only when EXACT recall +is requested AND the pairs fit (M <= dim) AND D*D is within the memory budget; else FHRR past the load knee; else cheap +HRR. All deciders known up front (an integer, a boolean, a budget) -- no estimate, so no detector-reliability risk. + +Extended holographic_loadmemory.AdaptiveRoleFillerMemory with the tensor backend (buffers the key/value atoms, seals a +TensorBindMemory on first recall, recalls by argmax over the stored fillers). MEASURED: tensor 200/200 EXACT at N=200 +where FHRR at the same load is well under half. The budget cap is respected -- exact requested but D*D over max_numbers +falls back to the best affordable lossy option (honest: you asked for exact but couldn't pay for it). Updated +mind.adaptive_record(expected_pairs, exact, max_numbers); refreshed the catalog entry. +4 tests (tensor exact, budget +cap, tensor-beats-fhrr-at-load; tensor integration). + +RE-ENABLE SCOREBOARD: PASS closed-form iterate (exact in regime), PASS FHRR-at-high-load (no harm mode), PASS tensor +exact recall (no harm mode on recall, storage-gated), FAIL projection denoise (kept shelved), OUT sparse eigenbasis +(scipy). The pattern holds: the SAFE re-enables are the ones with no harm mode -- the superior method is exactly correct +or strictly-better in its regime, so the only question is when the win is worth the cost (storage / complex domain), +which a known parameter answers. Remaining Group A: brdf multi-scatter (a BUILD first -- the Kulla-Conty term), sbc +many-factor knee, mixture phase-field on the tension dial. Group B (coarse-residual, all quality-tradeoffs so they need +denoise-level scrutiny): nystrom, splat 3DGS, volint marching, adaptive AA, raymarch over-relax. + +## RE-ENABLE #4: multi-scatter GGX (Kulla-Conty), gated by roughness -- plus two "already enabled" findings [+6 tests] + +Probed three more audit candidates (probe-first). Two were ALREADY ENABLED in the live code -- the audit +(auto-generated from NOTES) listed them as shelved, but the codebase already re-enabled both, like the consolidation +re-audits: + + * SBC resonator: ALREADY THE DEFAULT factorizer. mind.factor_composite prefers holographic_sbc.decompose_structure + (the validated SBC resonator) and DEPRECATES the dense ResonatorNetwork path; decompose_structure is a first-class + faculty. My measurement justifies that existing choice: at matched compute budget (6x50 iters), SBC matches or + BEATS the dense resonator at its big budget (20x400) on high-factor problems -- ~26x more compute-efficient at + high load, equal-or-better accuracy. Nothing to rebuild. + * mixture phase-field (immiscible double-well): ALREADY GATED by the tension dial. matter_step does + `if mix.tension: phi += dt*tension*(-double_well(phi) + 0.5*lap)` -- tension==0 skips the double-well (cheap + miscible blend), tension>0 applies the phase-field (sharp oil/water interface). The tension dial IS the parameter + gate the audit proposed. Nothing to rebuild. + +GENUINELY-UNBUILT + shipped: BRDF MULTI-SCATTER (Kulla-Conty 2017), gated by roughness. Single-scatter GGX drops +inter-microfacet bounces, so a rough METAL loses real energy -- MEASURED white-furnace at metallic=1: 0.85 @ r=0.2 +down to 0.36 @ r=1.0 (dielectrics hide it behind the diffuse term; metals lose ~50%). Built the Kulla-Conty +compensation: bake the single-scatter directional albedo E(mu) per roughness (cached), then f_ms = (1-E(v))(1-E(l)) / +(pi (1-E_avg)) tinted by F_avg, added as f_ms*N.L. MEASURED: white-furnace energy restored 0.36->1.00 @ r=1.0, +0.50->1.01 @ r=0.8. + +HONEST NUANCE (not a pure no-harm-mode win): the analytic term OVERSHOOTS at low roughness (1.16 @ r=0.2) where +E_avg~1 makes (1-E_avg) tiny -- over-brightening. That is exactly why the roughness GATE exists. Unlike the denoise +gate, the detector is an EXACT parameter (roughness), not an estimate, so the gate is reliable: MEASURED crossover ~ +r=0.22-0.25 (below, single-scatter is closer to conserving; above, multi-scatter wins big). Set MS_ROUGHNESS_GATE=0.25; +the gated |energy-1| is <= single-scatter at EVERY roughness (0.27->0.27 @ r=0.15 unchanged, 0.61->0.003 @ r=1.0). +Kept BACKWARD-COMPATIBLE: cook_torrance_ms / brdf_gated are opt-in; the default renderer is unchanged (energy +compensation changes rendered brightness, so it must be opt-in). Registered "Multi-scatter BRDF (re-enable)" in the +catalog. +6 tests (single-scatter loss, multi restores energy, gate routing, gated-never-worse, term-only-adds; +integration). + +RE-ENABLE SCOREBOARD: PASS iterate (exact), PASS FHRR (no harm mode), PASS tensor (no harm mode on recall), PASS +multi-scatter BRDF (exact detector, gated to avoid the overshoot); ALREADY-DONE sbc + mixture phase-field; FAIL +projection denoise (unreliable detector); OUT sparse eigenbasis (scipy). Remaining Group B (coarse-residual, +quality-tradeoffs): nystrom, splat 3DGS, volint marching, adaptive AA, raymarch over-relax -- all wait on the +coarse-first pass and need denoise-level scrutiny. + +## RE-ENABLE #5: the COARSE-FIRST residual pass (the Group-B unlocker) + an honest adaptive-AA caveat [+7 tests] + +Group B of the audit (nystrom, splat 3DGS refine, volint marching, adaptive AA, over-relaxed tracing) all share ONE +detector: run the cheap method, measure a per-cell residual/uncertainty, escalate to the expensive method only where +it's high. Built that shared primitive: holographic_coarsefirst -- + * escalate_mask(uncertainty, frac|threshold) -> WHERE to refine (exact top-k by frac, or >= an absolute threshold). + * refine_where_uncertain(coarse, uncertainty, refine_fn, frac) -> run refine_fn ONLY on the flagged cells, merge + into the coarse base (which is always kept). Returns (refined, mask, n). + * gradient_uncertainty(field) -> a generic cheap signal (local gradient = probably under-resolved). + * concentration(uncertainty) -> the honest breakeven check. + +MEASURED (adaptive field approximation -- the shape nystrom/splat/volint share): a smooth field with a thin ridge, +coarse RMSE 0.051 -> 0.006 after refining only ~20% of cells (~26% of the field evaluated). Clean 8x error drop at a +quarter of the cost, BECAUSE the uncertainty is concentrated (concentration 0.59). + +TWO honest findings kept: + * DEGENERATE-MASK BUG found+fixed: a value-threshold at the k-th largest blew up when the cut landed on a common + baseline (0) -- every cell >= 0 got flagged. Switched the frac path to an exact stable top-k. (A reminder that + conservative ">=" needs care around ties.) + * ADAPTIVE PATH-TRACE AA does NOT cleanly win at equal budget on simple scenes (MEASURED): a big diffuse sphere has + UNIFORM GI noise (concentration ~0.57, adaptive 0.019 vs uniform 0.015 RMSE -> uniform wins); a small object has + concentrated variance (concentration ~1.0) yet adaptive only TIES, because the object is a small fraction of the + whole-image error and the sky is free for both. So concentration() is NECESSARY, not sufficient: low concentration + rules coarse-first OUT; high concentration makes it a CANDIDATE that still owes a measured win where the hard region + also carries the error. The pathtracer already exposes the mechanism (return_variance + active mask); the primitive + plugs straight in, but the SCENE decides whether it pays. Kept as a loud caveat, not shipped as an auto-default. + +Registered "Coarse-first refine (re-enable)" in the catalog. +7 tests (mask by frac/threshold, refine touches only +flagged, field-approx win, concentration low-for-uniform / high-for-spike, gradient flags edges; integration). + +RE-ENABLE SCOREBOARD: PASS iterate / FHRR / tensor / multi-scatter BRDF; PASS the coarse-first PRIMITIVE (the Group-B +detector, measured on field approximation); ALREADY-DONE sbc + mixture; FAIL projection denoise; CAVEAT adaptive +path-trace AA (mechanism wired, win is scene-dependent -- needs concentrated error, not just concentrated variance); +OUT sparse eigenbasis. The remaining Group-B candidates (nystrom, splat refine, volint) can now be built ON the +coarse-first primitive -- each still owes its own measured breakeven, and concentration() is the tool to pre-check it. + +## RE-ENABLE #6: Nystrom for low-rank kernels, gated by a probe residual [+6 tests] + +Second Group-B candidate, and the FIRST with a clean-enough detector to ship (the audit put it in Group B, needing a +residual probe). Nystrom applies a kernel-weighted field in O(N*m) via m landmarks instead of exact O(N^2), but it is +exact only when the kernel is LOW-RANK (a smooth field, sigma not tiny) -- the kept negative. + +THE DETECTOR (measured RELIABLE, unlike the adaptive-AA variance): compute BOTH exact and Nystrom on a small held-out +PROBE (a few points, cheap) and take their relative error. MEASURED across sigma: the probe error tracks the FULL-field +error closely (0.06 vs 0.06 at the good end, 0.58 vs 0.63 at the bad end); even where it diverges at tiny sigma both are +high, so the gate still correctly falls back. The probe costs O(probe*Ns) -- a few percent of the full exact -- so a +small overhead when we fall back and a big win when Nystrom is safe. + +BUILT apply_kernel_gated(points, sources, weights, sigma, m, threshold=0.1): probe the low-rank-ness; if the probe error +<= threshold use Nystrom (O(N*m)), else fall back to EXACT (O(N^2)). Uses RegimeGate with above=False (superior=cheap +Nystrom in the low-rank regime; fallback=exact, the always-correct safe default). MEASURED speedup on a smooth kernel: +6.6x @ N=800, 13.9x @ N=2000 (the win grows with N), rel-err ~0.000-0.002. On a sharp kernel it falls back to exact +(byte-correct). KEY: because the fallback is EXACT, the gate can never be WRONG -- at worst (probe under-reads) it is a +little slower than optimal. That is why this ships where projection-denoise did not: the exact fallback removes the harm +mode entirely, and the detector is reliable besides. + +Registered "Nystrom kernel (re-enable)" in the catalog. +6 tests (low-rank->nystrom accurate, high-rank->exact +byte-correct, probe tracks regime, result-always-accurate across sigma, info reporting; integration with measured +speedup). + +RE-ENABLE SCOREBOARD: PASS iterate / FHRR / tensor / multi-scatter BRDF / coarse-first primitive / Nystrom low-rank +gate; ALREADY-DONE sbc + mixture; FAIL projection denoise; CAVEAT adaptive path-trace AA; OUT sparse eigenbasis. The +Nystrom win reinforces the rule: a re-enable ships cleanly when (a) its detector is reliable AND (b) it has a safe +(exact / strictly-better) fallback -- Nystrom has both. Remaining Group B: splat 3DGS refine and volint marching +(both build on the coarse-first primitive; each still owes a measured breakeven). + +## RE-ENABLE #7: full-3DGS anisotropic splat refinement, composed coarse-first [+5 tests] + +Third Group-B candidate. splat_fit is a cheap isotropic matching pursuit; an isotropic blob cannot represent a sharp or +oriented feature, so it leaves residual there. aniso_fit (gradient descent on anisotropic covariances) can -- so the +re-enable composes them COARSE-FIRST: fit the cheap isotropic base, then anisotropic-refine what it MISSED (its +residual). Built splat_refine_residual(target, iso_splats, K_aniso, steps) and fit_coarse_first(target, K_iso, K_aniso). + +MEASURED: on a sharp diagonal edge, iso 15.7 dB -> coarse-first 20.0 dB (+4.4). At EQUAL wall-clock, iso+aniso-refine +(20.0) beats spending the same time on more isotropic splats (18.7). NO HARM MODE (measured across sharp/smooth/ridge/ +random targets): refining the residual only adds detail, so it is always >= the isotropic baseline (sharp +4.4, smooth ++0.3, ridge +1.5, noise +0.3 -- never negative). + +TWO honest findings kept: + * concentration() is the WRONG detector here, and BACKWARDS: an anisotropic edge spreads its residual ALONG the edge + (many cells, low POINT-concentration = 0.16), while a smooth blob's residual is point-concentrated (0.34). So the + coarse-first concentration signal (great for point-like structure) does not detect anisotropy. + * a structure-tensor anisotropy score did not separate the cases cleanly either (sharp 0.41, ridge 0.51, smooth 0.34 + -- too much overlap). So there is NO reliable cheap detector for WHEN anisotropic refinement is the best use of + compute. +BECAUSE it has no harm mode, it does not NEED a detector to be safe -- shipped as an always-safe OPT-IN refinement (the +caller applies it for higher fidelity when it can afford the anisotropic fit), NOT an auto-gate. Contrast the +parameter-gated re-enables (iterate/FHRR/tensor/BRDF/Nystrom), which needed a reliable detector because a wrong choice +there would cost quality or correctness. + +Registered "Splat aniso-refine (re-enable)" in the catalog. +5 tests (beats-iso-on-sharp-edge, no-harm-mode across +targets, returns render+splats, concentration-is-backwards; integration). + +RE-ENABLE SCOREBOARD: PASS iterate / FHRR / tensor / multi-scatter BRDF / coarse-first primitive / Nystrom / splat +aniso-refine (opt-in, no harm mode); ALREADY-DONE sbc + mixture; FAIL projection denoise; CAVEAT adaptive path-trace AA; +OUT sparse eigenbasis. The rule now has TWO safe-to-ship shapes: (1) a reliable detector + a safe fallback (Nystrom, +BRDF), or (2) NO harm mode at all, so no detector is needed (splat aniso-refine, FHRR, tensor). Only volint marching +remains of the original Group B. + +## QUERY BACKLOG batch 1: probe-first sweep + the history family (P7-P12) wired [+8 tests] + +New backlog (holostuff_query_backlog.md): harden the query layer to a real database + promote the differentiated +faculties. PROBED the LIVE query module first (it grew 651 -> 1323 lines since the backlog snapshot, so a large +fraction was ALREADY BUILT -- exactly the backlog's own "never re-request what ships" rule): + + ALREADY DONE (verified working, not rebuilt): + * FIX tier F1/F2/F3: unknown-column now raises QueryError; LIMIT 0 returns no rows; AND/OR in WHERE WORKS (B3). + * B1 JOIN (join()/`_joined_row`, inner+outer), B2 UPDATE/DELETE/compact (tombstone `_deleted` + replay GC), + B4 primary-key index (set_primary_key/pk_lookup, O(1)), B5 constraints (PK-unique/NOT NULL/UNIQUE/FK/CHECK via + ConstraintError), B9 OFFSET/DISTINCT/OR -- all present and measured working. + * P1-P6 similarity family (similar_to/cluster/anomalies/near_duplicates/explain_match/recommend) -- all shipped as + module functions and working (cluster reports coherence; anomalies calibrated; explain_match shows which columns + drive the match). + + GENUINELY MISSING -> BUILT THIS BATCH: the history family P7-P12 (query.py imports only the kernel, so the + versioning faculties were never wired to the tables). New module holographic_querytime.py -- a git-like timeline + for a query Table that REUSES the shipped faculties (VersionedStore vectors, DeltaChain diff, CompositionTree + tamper-locate): + P7 select_as_of(h, version, sql) -- TIME TRAVEL: checkout the past table, run an ordinary SELECT (measured: a + balance reads 100 at v0, 250 at v2). + P8 history_of(h, pk_col, key) -- BLAME: a row's value timeline across versions (None before it existed). + P9 diff_versions(h, a, b) -- added/removed/changed with FIELD-level detail; reuses DeltaChain._changed_rows + for the vector cross-check when shapes match. + P10 revert_to(h, version) -- git-style undo (append-only; never rewrites history). + P11 branch/compare/discard -- git-for-data what-ifs; MERGE deferred (needs an explicit conflict policy). + P12 prove(h, v)/find_tampering -- a compact Merkle digest per version + CompositionTree.locate names the altered + row in O(log n) (measured: tamper row 1 -> located row 1; untampered -> None). + Registered "Query time-travel & audit" in the catalog. +8 tests (one per verb + determinism; integration). + +KEPT NEGATIVES (loud): a commit snapshots the record matrix + row dicts (O(rows)/version -- the delta-compressed +vector store is available underneath, but exact time-travel needs the lossless row values); revert is append-only; +branch ships without merge. Still genuinely TODO in the backlog: P13 (verify+doc), PR1-PR6 (installable VSA programs), +WS1-WS7 (tiered workspaces/folders), B6/B7 (transactions/durability -- a `transaction` ctx + Rollback already exist, +to be verified next), B10 (graph, with the loud recall-collapse caveat). + +## QUERY BACKLOG batch 2: B6/B7 + P13 verified done; PR programs (PR1-PR6) built [+8 tests] + +Continued the probe-first sweep, then built the PR wave. + +VERIFIED ALREADY DONE (not rebuilt): + * B6 transactions -- `with transaction(t): ...` snapshots on entry, commits on clean exit, and rolls the WHOLE + batch back on any exception (swallows `raise Rollback`, re-raises real errors). Measured: an explicit Rollback, + a ValueError, and a mid-transaction ConstraintError each reverted an earlier in-txn update too (true atomicity). + * P13 add-column-no-migration -- `add_column` appends a role; old rows stay sparse, new rows carry it, no rewrite. + * B7 durability -- to_state/from_state give save+replay; fsync-per-batch is an ops detail outside the NumPy core. + +BUILT: holographic_queryprog.py -- VSA programs as installable database objects (stored procedures, VSA-native). A +program is a hypervector the HoloMachine runs (LOAD/BIND/APPLY/HALT -- not arbitrary code), so it can't do I/O or run +host code and can only call the whitelisted handlers it was installed with (safer than a SQL stored procedure). Reuses +the shipped machine.assemble/run and explain_program. + PR1 list_programs -- the catalog as queryable rows (name/domain/doc/tier), like pg_proc. + PR3 explain -- a handler-less DRY RUN: which faculties it WOULD call, how many steps (no execution). + PR4 find(text) -- find a program BY MEANING (doc word-overlap descriptor, cosine-ranked with confidence) -- a SQL + catalog is exact-match only. Measured: "group a series into clusters" ranks the clustering program first. + PR5 install -- CREATE FUNCTION: assemble the instructions to a program vector, merge the program's symbols + SCOPED-by-name into the shared vocab (self-discovery), catalog the metadata. + PR6 execute -- run a program over query rows: encode rows -> accumulator, run with ONLY the whitelisted handlers + (sandbox) and a step bound (no runaway), decode with a calibrated confidence (garbage shows LOW confidence). +Registered "VSA programs as DB objects" in the catalog (47 homes). +8 tests (install/list/find/explain/execute/ +sandbox/step-bound/system-read-only; integration). + +KEPT NEGATIVES (loud): find-by-meaning is WORD-OVERLAP over docs, not a language model; execute results are on the +fuzzy side so they carry a confidence and can abstain; symbols are scoped per program to avoid a flat-namespace +collision; system-tier programs are read-only (uninstall refused). Remaining in the backlog: WS1-WS7 (tiered +workspaces/folders), B8 (concurrency), B10 (graph traversal, with the loud recall-collapse caveat). + +## QUERY BACKLOG batch 3: WS tier -- most already shipped; folders (WS7) + combine-scenes (WS6) built [+8 tests] + +Probed the workspace/persistence tier. Nearly all of it was ALREADY BUILT (holographic_workspace.py, 199 ln): + * WS1 tiers -- add_namespace(tier=)/create_namespace/drop_namespace(protects system)/tier_of: three-tier model + (system read-only / persistent durable / workspace transient) at the _require_writable chokepoint. DONE. + * WS2 tier-scoped save -- to_state(tiers=['persistent']) filters by tier. DONE. + * WS3 Workspace + WorkspaceManager (new/switch/clear/active). DONE. + * WS4 reset_to_default -- verified: persistent data survives, workspaces cleared, system restored. DONE. + * WS5 export/import_workspace -- round-trips. DONE. + * WS6 combine_workspaces -- unions two workspaces' tables with an explicit collision policy (error/suffix/left/ + right). DONE. But the SCENE-level combine was explicitly deferred ("left to those handles") -> built below. + +GENUINELY MISSING -> BUILT THIS BATCH: + * WS7 folders (holographic_queryfolder.py): a shallow grouping tree over the tables (database > folder > table). + A folder REFERENCES existing tables, never copies them. The load-bearing decision: HOME folder = ownership (one + per table, governs lifecycle -- dropping it deletes its tables) vs ASSOCIATION link = grouping (many per table, + unlink never deletes). tables_in(folder, recursive) powers scoped search (drill-down = a smaller candidate set). + Measured: drop_folder deletes only the HOME tables; a table linked elsewhere survives; unlink from home is + refused (would orphan it); a folder can't reference a table that doesn't exist. Honours the system read-only wall. + * WS6 combine-scenes (SceneCoder.combine): a scene IS a bundle (unnormalised superposition of object products), so + combining two scenes is vector ADDITION -- the object counts add (norm^2 ~ n), the resonator still recovers any + object. Measured: 2 objects + 1 object -> 3. Kept negative: two IDENTICAL objects superpose (amplitude doubles) + rather than staying distinct -- re-key an id if duplicates must be kept separate. + Registered "Workspace folders" in the catalog (48 homes). +8 tests (folder ownership/association/resolve/unlink/ + drop/scoped-recursive + combine-is-a-bundle; integration). + +KEPT NEGATIVES (loud): keep the folder tree SHALLOW (depth is as annoying as a flat pile); an association is a +grouping not a copy (edits seen everywhere, lifecycle follows home only). Remaining in the whole query backlog: B8 +(concurrency -- single-writer+snapshot), B10 (graph traversal, with the loud recall-collapse-at-scale caveat). + +## QUERY BACKLOG batch 4 (FINAL): B8 concurrency + B10 graph traversal -- backlog complete [+11 tests] + +The last two, both use-case-dependent and honestly caveated in the backlog. + + * B10 graph traversal (holographic_querygraph.py): reachability over a table's edges -- neighbors, descendants, + reachable, shortest path (BFS) -- what recursive SQL CTEs make painful. Built on an EXACT adjacency index BY + DESIGN: the holographic graph memory has a measured recall collapse at scale (routed descent overloads like an + over-full maze field), so traversal uses a plain deterministic dict-of-neighbours, which scales as O(V+E) and + returns exact answers -- no recall cliff. Tombstone-aware (skips _deleted edge rows), directed or undirected. + Measured: descendants(1)={2,3,4,5}, shortest path 1->5 length 4, reachable is directional, undirected reaches back. + LOUD KEPT NEGATIVE: exact index on purpose -- the holographic graph store is NOT used for traversal (it stays for + classification by routed descent, where it belongs). + * B8 concurrency (holographic_querylock.py): single-writer + snapshot readers. One writer at a time (exclusive + threading.Lock; a second writer waits with block=True or fails fast with ConcurrencyError on block=False), and + lock-free reader SNAPSHOTS -- a consistent point-in-time copy immune to later writes (reusing the B6 snapshot). + Measured: a snapshot taken before a write still reads the OLD value while a fresh read sees the new one; the lock + releases on exit. HONEST SCOPE: concurrent-WRITER isolation (MVCC) is DEFERRED -- this single-writer model is the + correct simple first step and covers most workloads; do not advertise serialisable multi-writer isolation. + Registered "Graph traversal (exact)" + "Single-writer concurrency" in the catalog (50 homes). +11 tests. + +=== QUERY BACKLOG COMPLETE === +Across four batches: FIX (F1-F3), B1-B7 (JOIN/UPDATE-DELETE/multi-WHERE/PK-index/constraints/transactions/durability), +B8-B10 (concurrency/SQL-surface/graph), P1-P13 (similarity family + history family + add-column), PR1-PR6 (installable +VSA programs), WS1-WS7 (tiered workspaces + folders). The striking part, per the backlog's own probe-first rule: the +query module had grown 651 -> 1323 lines plus a full workspace module BEFORE this engagement, so the large majority was +ALREADY BUILT -- the genuinely-new work was the history family (querytime), the programs layer (queryprog), folders +(queryfolder), graph traversal (querygraph), and the writer lock (querylock). Everything else was verify-and-document. + +## DISTRIBUTED COORDINATOR batch 1: R2 Coordinator + LocalPool + margin-gated tie-break [+9 tests] + +New backlog (leCore_distributed_coordinator_backlog.md): a distributed compute Coordinator with PLUGGABLE BACKENDS, +sitting BEHIND holographic_distribute (which already holds the render-farm theory: partition -> worker -> shared +read-only cache -> monoid reduce, all stdlib). Probe confirmed distribute has partition/adaptive_partition(LPT)/ +distribute/reduce_sum/min/max and runs workers sequentially in-process; no coordinator existed. + +BUILT holographic_coordinator.py (R2, the base case -- useful with zero network nodes): + * Coordinator(backend).run(buckets, worker, cache, reduce) -- backend-agnostic schedule + collect + monoid reduce; + releases the shared cache in a finally even if a worker raises. + * InProcessBackend -- sequential default (mirrors distribute, always available, the reference). + * LocalPool(n) -- a PERSISTENT ProcessPoolExecutor (each worker its own interpreter + GIL) + shared_memory for the + read-only cache (a numpy array is written ONCE, workers map it read-only by name -- no per-bucket pickling). The + child re-attaches via a top-level _run_with_cache trampoline (picklable by reference); the parent owns unlink(). + * decide(sims, safe_margin) / decide_sequence -- the MARGIN-GATED canonical tie-break: cleanup already computed the + sims so the margin is free; a comfortable margin trusts the fast/distributed argmax, only the rare knife-edge is + resolved by determinism.argmax_tiebreak so every node agrees (a RULE breaks the tie, not the ~1e-12 float wobble). + +MEASURED (honest): + * Correctness: LocalPool workers run in SEPARATE interpreters (distinct PIDs from the parent -- verified); MIN + reassembly is BIT-EXACT vs in-process; a shared-cache SUM matches a direct compute. + * SPEED / KEPT NEGATIVE: this sandbox has ONE CPU core, so process parallelism gives NO wall-clock speedup here + (workers time-slice on the single core; the pool even loses slightly on light work -- exactly the "offload COARSE, + not fine" rule: IPC + spawn cost dominates unless a bucket is compute-heavy). The mechanism is correct and will + parallelize on a multi-core box; the win cannot be shown on 1 core. Documented loudly rather than faked. + +Registered "Distributed coordinator" in the catalog (51 homes). +9 tests (in-proc reduce, shared cache, separate PIDs, +MIN bit-exact across backends, decide margin gate, decide identical under reduction wobble, decide_sequence, cache +released on error; integration bit-exact + tie-break). Remaining rungs: R4 command backend + orchestrator Tools, R3 +network farm (worker daemon + socket/json), R5 hardening (stragglers/retry/redundant-compute voting). + +## DISTRIBUTED COORDINATOR batch 2: R4 command backend + orchestrator Tools [+8 tests] + +BUILT holographic_command.py (R4 -- run ANY program/script, the door to external tools/services): + * CommandRunner -- run a registered ALLOWLISTED command via subprocess, capture stdout/stderr/exit, TIME-BOXED. + SECURITY (the highest-risk surface, kept loud): allowlist ONLY (a command runs only if its NAME was registered; + no path from input to a new command); NEVER a shell (subprocess argv list, shell=False -> no globbing/`;`/pipe + injection -- an arg value is one argv element, can't become a new command); VALUES fill "{key}" placeholders, + they don't build the command; every run has a timeout. Measured: "hello; rm -rf /" echoes as a LITERAL (the ';' + is not interpreted); an unregistered name is refused; a hung command is killed at the timeout. + * command_as_tool(runner, name, ...) -- wrap an allowlisted command as an orchestrator Tool (fn returns stdout, + raises on non-zero exit) so the Planner can select+chain it and the CircuitBreaker trips on a flaky one -- an + external program joins the same VSA fabric as an internal faculty. Measured: a two-step external chain + (upper -> reverse) runs; a failing command opens its breaker so the planner skips it. + Registered "Command runner (external tools)" in the catalog (52 homes). +8 tests (allowlist gate, no-shell + injection, capture, missing-placeholder, unknown-exe-at-register, timeout, tool+breaker; integration Planner chain). + +The sandboxing beyond allowlist+no-shell+timeout (restricted user / container / no-network) is an OPERATIONAL +responsibility, stated as such -- this module gives the in-process safety rails. Remaining rungs: R3 network farm +(worker daemon + socket/json + cache shipping), R5 hardening (stragglers/retry/redundant-compute voting + auth). + +## DISTRIBUTED COORDINATOR batch 3: R3 network farm (run on another machine) [+8 tests] + +BUILT holographic_farm.py (R3 -- the render farm / SETI@home rung; stdlib http.server + json + base64, no framework): + * WorkerDaemon -- runs ON a node. A registry of NAMED workers (YOUR trusted code) + a cache store keyed by content + hash. Endpoints /ping (health + a measured speed for LPT), /cache (receive the read-only cache ONCE, keep by hash), + /task (run a named worker on a bucket). Runnable STANDALONE: `python holographic_farm.py --host 0.0.0.0 --port N` + starts a node another machine's coordinator can dispatch to. + * NetworkFarm -- a Coordinator BACKEND. Registers nodes (pings for speed), publishes the cache once per node by + content hash (nodes reuse it), and dispatches buckets CONCURRENTLY via a thread pool (http I/O releases the GIL). + Plugs into the SAME Coordinator.run() as the local pool -- only WHERE the worker runs changes. + So Coordinator(NetworkFarm([addr])).run(buckets, "worker_name", cache, reduce) fans a monoid job across machines. + +MEASURED: over the real http/json path -- network SUM matches a direct compute; MIN reassembly is BIT-EXACT over the +network; the cache is shipped ONCE and kept by content hash; an unregistered worker name is REFUSED; a worker error is +reported, not crashed. END-TO-END proof of "run on another machine": started the daemon as a genuinely SEPARATE PROCESS +(its own PID) and a coordinator in ANOTHER process dispatched a real job over the socket and got the correct result -- +loopback here stands in for a remote host; point --host at the remote IP and it is a real farm. + +LOAD-BEARING SECURITY (loud): buckets are DATA, workers are REGISTERED CODE -- the coordinator sends a worker NAME + a +bucket, never code; a daemon runs only its registered workers. Do NOT expose a daemon openly (bind local / auth / TLS +across the internet -- deployment responsibility, stated). UNTRUSTED nodes still need redundant-compute + voting (R5) -- +a node can faithfully return a plausible WRONG answer, which repair does not catch; this backend is for a TRUSTED farm +until R5. Registered "Network render farm" in the catalog (53 homes). +8 tests. Remaining: R5 hardening +(stragglers/backup exec, retry+reassign, redundant-compute voting + canary buckets + auth before untrusted nodes). + +## STANDALONE API SERVICE: run on any OS, talk via HTTP/JSON [+7 tests] + +Moose: run the app standalone on any supported OS, communicated with via an API. BUILT holographic_service.py -- a +STDLIB-ONLY HTTP/JSON service (http.server + json; numpy is the only real dep, which the engine needs anyway, so it +runs on any Python 3 with near-zero setup). A tiny route registry (method, path) -> handler makes the surface read top +to bottom and adds an endpoint in one line. + + GET / -- self-describing index of the endpoints + GET /health -- {ok, name, version, python, platform, capabilities} + GET /capabilities -- every advertised capability (name + description) + POST /capabilities/search -- {query} -> matching capability homes (plain-English discovery) + POST /sql -- {sql} -> run CREATE/INSERT/SELECT against the service's VSA Database (the whole query + layer, over HTTP) -- clean 400 on bad SQL, 404 unknown route, 500 unexpected. +Optional --token: a bearer-token gate (401 without it). Binds 127.0.0.1 by DEFAULT; --host 0.0.0.0 exposes it (only +behind auth/TLS on a trusted network -- stated loud). + +CROSS-OS LAUNCHERS (readable, mirror the existing run.bat): + * serve.sh (Linux/macOS, POSIX sh, LF) -- find python3, set PYTHONHASHSEED=0, ensure numpy, run the service; passes + --host/--port/--token through. + * serve.bat (Windows, CRLF) -- same, matching run.bat's style. + +MEASURED end-to-end over REAL HTTP: started the service as a subprocess and drove it with urllib -- /health returns +version+platform, the token gate returns 401 without the bearer and 200 with it, capability search finds the right +home, and CREATE/INSERT/SELECT round-trips through /sql with WHERE filtering. serve.sh launches the server and answers +/health. Registered "Standalone API service" in the catalog (54 homes). +7 tests (routes, SQL CRUD, error statuses, +real-http round-trip, token gate over http; integration). Reference: SERVICE.md (endpoints, curl examples, security). + +## STANDALONE DB: full SQL surface + GraphQL + persistence over the API [+7 tests, +1 join fix] + +Moose: make the SQL/GraphQL work in standalone mode so leCore can be a DROP-IN DATABASE for other apps. The API service +now exposes the WHOLE database, not just CREATE/INSERT/SELECT: + + * EXTENDED run_db_sql (holographic_query.py) with the writes an app needs day one: + - UPDATE ns.t SET c=v,... WHERE ... (delegates to update(); WHERE REQUIRED -- a networked endpoint must not let a + typo rewrite a whole table) + - DELETE FROM ns.t WHERE ... (delegates to delete(); WHERE required, same guard) + - SELECT cols FROM a [LEFT] JOIN b ON key [WHERE c op v] (delegates to join(), then projects + filters) + - DROP TABLE ns.t + Readable helper fns _run_update / _run_delete / _run_join + _cmp, thoroughly commented. + * FIXED a real join bug found by the new integration test: join() iterated ALL rows including tombstoned ones, so an + UPDATE (which tombstones the old row + re-inserts) made a join DOUBLE-COUNT. Now skips _deleted rows on both sides. + * SERVICE endpoints added: POST /graphql (resolve a GraphQL query over nested documents -- the natural fit for nested + data, via holographic_graphql.Scene/resolve), POST/GET /documents (the document store), POST /save + /load (disk + persistence), and --persist FILE (auto-load on start, auto-save after every write). + +MEASURED as a real DB replacement: over REAL HTTP across TWO separate processes -- session 1 wrote accounts (incl. an +UPDATE) + documents with --persist, was KILLED; session 2 (a fresh process) read back the SQL rows (the UPDATE +survived: alice 250) AND the GraphQL documents. to_state/from_state are JSON-serialisable (deterministic replay), so +the whole store round-trips through one JSON file. Updated the catalog entry to "standalone DATABASE server ... drop-in +DB replacement". +7 service tests (full SQL, join, WHERE-required, graphql, persistence-survives-restart, save/load) + +1 query test (extended run_db_sql) + a full-surface HTTP integration test. SERVICE.md documents every endpoint + curl +examples + the WHERE-required guard. + +## DISTRIBUTED COORDINATOR batch 4 (FINAL): R5 hardening -- backlog complete [+12 tests] + +BUILT holographic_hardening.py (R5 -- fault tolerance + verification, the BOINC/SETI@home discipline, mandatory before +untrusted farm nodes). Composes on top of any Coordinator backend via the same submit()->future interface: + * agree(results, tol, quorum) -- majority VOTING: group results by approximate equality (allclose for arrays, tol + for numbers, == otherwise), accept the largest group only if it reaches a quorum (strict majority by default), + else raise NoConsensus (abstain, don't guess). This is the defense a node can't beat: it can't FORCE a result. + * retrying(make_and_wait, attempts, backoff) -- retry with exponential backoff; each attempt RE-SUBMITS, so a + reissue reassigns a dead node's work to whatever node the backend picks next. + * HardenedCoordinator(backend, redundancy, attempts, backoff, tol) -- .run() resolves every bucket with retry, + optionally REDUNDANTLY (redundancy independent copies) then votes, and can gate the run on CANARY buckets first + (known-answer spot checks; a wrong canary -> CanaryFailed, the whole run rejected). redundancy=1 = trusted pool + (retry only); redundancy>1 = untrusted nodes. + * run_with_backups(...) -- speculative straggler execution: reissue a task still unfinished after a grace period, + take whichever copy finishes first (correctness-preserving; wall-time benefit needs idle workers on >1 core). + +MEASURED: voting accepts a clear majority and abstains on three-way disagreement; retry recovers after 2 simulated +node failures; a canary with a wrong answer rejects an untrusted worker; over the REAL network farm, redundancy=3 +runs three independent copies and accepts only on agreement; straggler backups keep the reduction correct. Needed one +tiny additive fix: gave the in-process _Immediate future a .done() so the future interface is uniform across backends. +Registered "Distributed hardening (R5)" in the catalog (55 homes). +12 tests (voting/arrays/no-consensus, retry +succeed+giveup, hardened-reissue, redundant-accept, canary reject+pass, straggler-correct, min-reduce; integration +over the farm). + +KEPT NEGATIVES (loud): voting catches a node returning a DIFFERENT wrong answer, not all nodes sharing a bug (that's +what canaries are for); redundancy multiplies compute (use only for untrusted nodes); straggler backups help only with +idle workers on >1 core. === DISTRIBUTED COORDINATOR BACKLOG COMPLETE: R2 local pool + R3 network farm + R4 command +backend + R5 hardening + the standalone API/DB service, one Coordinator + one monoid reduce at every rung. === + +## JOB LIFECYCLE: start/pause/resume/cancel, survive a restart, across the farm [+17 tests] + +Moose: start/stop/pause/resume/cancel long-running work (renders, sims, dataset processing) -- across the distributed +network AND across an app restart (start a render, pause, save, close, reopen, resume). BUILT holographic_jobs.py. + +The fit: a long job is distributed MONOID work -- buckets processed by a worker, combined by a commutative/associative +reducer (sum/min/max/bundle). That structure makes pause/resume clean: completed buckets fold into `partials`, and +because the reduce is order-independent, you can stop after ANY bucket and combine what you have with whatever you +finish later. So: + * PAUSE = stop dispatching at the next bucket boundary + checkpoint the partials; RESUME = process only the REMAINING + buckets and reduce everything; CANCEL = stop + mark cancelled. Cooperative control (checked between buckets), so + in-flight remote work is never killed mid-bucket -- pick the bucket size / dispatch `batch` for responsiveness. + * A checkpoint is just (buckets, worker NAME, reducer NAME, cache, done-indices, partials) -> ONE JSON file. So a + paused job SURVIVES the app closing: reopen, load_all(), resume. Workers are referenced by NAME (registered on the + manager / the farm daemon) so a restored job re-resolves its code -- same rule as the farm. + +Job(id, buckets, worker, reduce, cache) + JobManager(backend, store_dir): create / start(background=) / pause / resume +/ cancel / status / list / wait / result, save/load + load_all() (restore every checkpoint on startup). Runs over ANY +coordinator backend (InProcess / LocalPool / NetworkFarm) -- the same job code, only WHERE the buckets run changes. + +Wired into the standalone service: POST /jobs/create, /jobs/start, /jobs/pause, /jobs/resume, /jobs/cancel, +/jobs/status, /jobs/result, GET /jobs -- with checkpoints beside the --persist file and load_all() on startup, so jobs +survive a SERVICE restart too. + +MEASURED end-to-end: (1) deterministic restart -- a half-done PAUSED job saved, loaded into a FRESH manager, resumed +to a correct total (each bucket exactly once); (2) background start/pause/resume + cancel; (3) over the REAL network +farm -- a render paused mid-job at 4/10 tiles, reopened in a fresh manager, resumed the remaining 6 on the farm, +correct result; (4) over the REAL HTTP API across TWO app processes -- started a 30-tile render, paused at 8/30, KILLED +the app, restarted (the paused render reappeared in GET /jobs), resumed -> 435 = sum(0..29), no tile duplicated or +lost. Small additive change: gave backends a `by_name` flag so the job layer resolves a worker-by-name correctly +(callable for local, name for the farm). Registered "Job lifecycle control" in the catalog (56 homes). +17 tests (10 +jobs + 6 service jobs + integration). SERVICE.md documents the /jobs endpoints. + +## MATERIAL LIBRARY: wired up + discoverable, render + physical bridged [+9 tests] + +Moose: make sure our physical material library is wired up and discoverable -- organized, usable for RENDERING and for +SCIENTISTS; work with the data we already have (users can add their own). + +PROBE-FIRST caught a near-duplicate: I started building a new material library, then found the engine ALREADY has two +rich ones -- deleted my duplicate and worked with what's there (the project's own rule). Audit: + * RENDER: holographic_matlib.py -- 141 PBR appearance presets across 17 classes (metal/gem/wood/stone/glass/liquid/ + biome/...), each a render-ready PBRMaterial; catalog()/material()/by_class(). Already wired via UnifiedMind + surface_material()/material_catalog(). + * PHYSICAL: holographic_definitions.MATERIALS -- 33 materials with density/refractive/viscosity/youngs/sound_speed/ + specific_heat/phase (the numbers a SOLVER needs). Already wired via UnifiedMind physical_material(). + * 15 names in BOTH (gold, water, diamond, copper, ...), but the two libraries were NOT connected, and NEITHER had a + discoverable catalog home -- that was the actual gap. + +BUILT holographic_materialindex.py -- a thin BRIDGE + discovery layer over the two existing libraries (no data moved or +duplicated): material_info(name) -> the unified view (RENDER appearance AND PHYSICAL properties, whichever exist); +find_materials(query) -> keyword search across both (name/class/phase); all_materials()/summary() -> the roster + counts. +render_material()/physical_properties() reach each home library through one door. User data added to either home library +shows up here automatically. + +WIRED: UnifiedMind gained material_info() / find_materials() / materials() (bridge + discovery) beside the existing +physical_material()/surface_material()/material_catalog(). Registered "Material library (render + physical)" in the +catalog (57 homes) so find_capability('physical properties density refractive index') now surfaces it. + +MEASURED: material_info('gold') -> render class metal + metallic 1.0 AND physical density 19300; a render-only preset +(chrome) resolves appearance, a physical-only material (mercury) resolves its physics; the bridged render material +drives the real cook_torrance/glTF path; find_materials('gem crystal') -> diamond/emerald/quartz/... Registered +9 tests ++ integration (bridge render+science+discoverable). Kept scope note: find_materials is readable word-overlap (name/ +class/phase), not semantic; roughness in the render presets is a finish DEFAULT, not a physical constant. + +## PHYSICAL MATERIAL LIBRARY: hardened + expanded (33 -> 117, validated, categorized) [+13 tests] + +Moose: harden the physical material properties and expand the library to a comprehensive collection -- a good starting +point for a variety of users. Probed first: the physical library lived inline in holographic_definitions.MATERIALS (33 +materials, density/phase always + partial sound_speed/specific_heat/youngs/viscosity/refractive), consumed by +resolve_scenario (density-based buoyancy), physical_material, and the material index. + +BUILT holographic_materialdata.py -- a comprehensive, categorised, VALIDATED physical database (116 materials across 12 +categories: metal/liquid/gas/polymer/ceramic/glass/mineral/stone/wood/biological/building/semiconductor). Real standard +reference values (CRC/engineering tables, ~20 C 1 atm), SI throughout, with new fields beyond the legacy set: +thermal_conductivity (W/mK), thermal_expansion (1/K), melting_point (K), boiling_point (K), plus a `category`. A UNITS +dict documents every field's unit + meaning; validate() plausibility-checks every entry (units/ranges/category/phase) +and the library is CLEAN; field_coverage() reports honest partial coverage (refractive only on transparent media, etc.). +HONESTY kept loud: values are good STARTING POINTS, not lab certificates -- real materials vary with alloy/grade/temp/ +grain; where a property is strongly variable or uncertain the field is OMITTED, not guessed (a missing field means "look +it up for your material", not zero). + +MERGED into holographic_definitions.MATERIALS by ENRICHMENT: legacy entries get their MISSING fields added via +setdefault (so long-standing values are NEVER overwritten -- resolve_scenario's wood 117. + +WIRED the new richness through holographic_materialindex + UnifiedMind: physical_categories() / physical_by_category() / +physical_units() / validate_physical(); material_info() now attaches per-field UNITS so a value is self-describing; +UnifiedMind gained material_units() / materials_by_category() / validate_materials(). Updated the catalog entry (now +"~120 physical materials in 12 categories ... validated, unit-documented"). MEASURED: 23 metals + 10 gases + polymers/ +ceramics/etc.; validate clean; tungsten melts at 3695 K; diamond thermal conductivity 2200 W/mK; the material index now +bridges 141 render + 117 physical (31 in both). +13 tests (8 data + index accessors + integration) all green. Kept +scope note: values are representative starting points; users add their own via add_material()/editing MATERIALS. + +## VENDORED DICTIONARY + TAXONOMY: real world-knowledge for contextual awareness [+10 tests] + +Moose: the dictionary/encyclopedia used for text-gen testing should be wired up as a real thing for contextual +awareness; vendor a more comprehensive one if we can. Probed first: holographic_lexicon/encyclopedia and the mind's +learn_dictionary/define are MACHINERY that consume a {word: [def words]} map -- but NO dictionary shipped; the text-gen +tests DOWNLOAD NLTK Gutenberg books (fiction) on demand. So define() was empty until fed a dictionary. + +VENDORED a comprehensive one: Princeton WordNet 3.0 (via NLTK, free w/ attribution) -> data/knowledge/dictionary.json.gz +(144,478 words, 5.8 MB gzipped) + manifest.json + LICENSE_WORDNET.txt. Each word carries definition, part of speech, +synonyms, an example, and its is_a parent (hypernym) -- so ONE file is both a DICTIONARY and a TAXONOMY/encyclopedia. +NLTK was used ONCE at build time to extract; the runtime loader is STDLIB-ONLY (gzip+json, lazy), honouring the +NumPy/stdlib rule. + +BUILT holographic_dictionary.py: define/entry/synonyms/part_of_speech/example/search over the dictionary, is_a + +hypernym_chain over the taxonomy (dog -> domestic animal -> animal -> ... -> entity), and definition_map(vocab) -- the +bridge that feeds the mind's learn_dictionary from REAL definitions. WIRED into UnifiedMind: lookup(word) (real +world-knowledge, distinct from define()'s learned-meaning neighbours), word_taxonomy(word), dictionary_size(), and +learn_vocabulary(vocab) -- bootstrap the encoder's word meanings from the vendored dictionary. Registered "Dictionary + +taxonomy (vendored)" in the catalog (58 homes); find_capability('what does a word mean') surfaces it. + +MEASURED: 144k words; lookup('gravity') -> physics force-of-attraction + synonyms + is_a 'attraction'; word_taxonomy( +'dog') climbs to animal/entity; after learn_vocabulary(['car','truck','vehicle',...]) define('car') -> vehicle/truck +(real learned meaning). +10 tests + integration. Zip grows ~20 -> ~26 MB (the dictionary). Kept note: this is the +batteries-included default -- users can replace the vendored file or pass their own/larger dictionary to the same +machinery. RECOMMENDATIONS for other resources (formulas, programming refs, K-12, science papers, books) delivered +separately to Moose. + +## TOOL WIRING: 2D / text-gen / language-learning / utilities made discoverable (catalog-driven) [+7 tests] + +Moose: 2D editing/generation, text generation + learning, and utility/helper tools should be wired up and discoverable +-- and USE the catalog to find gaps faster. Did exactly that: instead of hand-auditing modules, posed the plain-English +queries a USER would ask and checked what find_capability returned. The gap was clear -- the TOOLS mostly existed as +mind faculties, but had NO curated homes, so natural queries returned only auto-docstring module hits or NOTHING +("draw a picture", "make a 2d drawing", "paint on a canvas" -> empty). + +FIX (discoverability, mostly): registered 4 CURATED catalog homes with rich, user-language aliases (62 homes total): + * "2D image editing & generation" -- recolor/colour-transfer, sharpen, svgf-denoise, downscale, blend/crossfade/morph, + procedural pattern_field, svg_canvas, image_archive, compare_images. Now the top hit for edit/generate/draw/paint/ + sharpen/downscale/recolor/crossfade/svg queries (the previously-empty ones included). + * "Text generation" -- generate/generate_structured/respond/answer. Top hit for generate/write/answer queries. + * "Language learning" -- learn_text/dictionary/vocabulary/encyclopedia/sequence (the curriculum). Top for + learn-from-corpus/curriculum/teach queries. + * "Utilities & helpers" -- uri (content address/hash), verify (tamper), fountain (erasure), deltachain, history + (rollback), compress/codec, determinism. Top for verify/erasure/content-address/version-history queries. + +WIRED (usability): two genuinely-useful 2D ops that existed as modules but weren't mind faculties -- recolor_image +(holographic_colortransfer.color_transfer) and blend_images (holographic_generate.crossfade_images). (Left pack/unpack +OUT: it is a delta packer for SIMILAR frames, lossy on arbitrary images -- exposing it as a general packer would +mislead; kept as a module for advanced use.) + +TOOL for the future: tools/catalog_gaps.py -- pose user-style queries per family, flag any with no curated home (only +'holographic_*' auto-hits or nothing). Reports 0 gaps for the four fixed families now. This is the reusable version of +the audit Moose suggested: the catalog finds discoverability gaps faster than grepping modules. +7 tests + integration. + +## AGENT-FRIENDLY LAYER: skills, suggest, confident routing, autocomplete [+9 tests] + +Moose: make the app agentic-friendly (and human-usable) -- suggested autocomplete, decision trees when confident, skill +descriptions. Also finished the cross-engine discoverability sweep (below). + +BUILT holographic_skills.py -- an agent layer over the catalog + UnifiedMind, deterministic + stdlib-only: + * SKILL DESCRIPTIONS: mind_methods() introspects EVERY public UnifiedMind method (via inspect) -> {name: signature, + summary}; skill_card(name) resolves a capability OR a method to a machine-readable card (what it does + how to + CALL it, real signature); manifest() is the whole surface (434 capabilities + ~700 methods) an agent loads once. + * SUGGEST / AUTOCOMPLETE: suggest(task) ranks capabilities for a plain-English task WITH a 0..1 confidence + the + concrete call; complete(prefix) autocompletes method names with signatures ('mind.learn_'). + * CONFIDENT ROUTING (decision node): route(task) -> {'decision':'act', skill, call} when one skill clearly wins, + else {'decision':'choose', options} when ambiguous, else {'decision':'unknown', prompt}. 'Act when confident, ask + when not', score-based -- never a silent guess. + * CONFIDENCE FIX (kept): computed among CURATED homes only -- an auto-registered module twin (holographic_jobs) is a + pointer to a capability, not a competing skill, so it must not dilute confidence. Genuine ambiguity between + DIFFERENT curated homes (e.g. the three distributed homes) still yields 'choose'. Added Catalog.find_scored() to + expose match scores for this. + +WIRED: UnifiedMind gained suggest/route/describe_skill/complete_method/skills (describe was already taken -> _skill). +The STANDALONE SERVICE gained GET /skills + POST /skills/suggest|route|complete|card -- so an AGENT driving the HTTP +API can discover the whole surface, get confidence-scored suggestions, and route decisions. Registered "Agent skills +(discover & route)" in the catalog (76 homes). MEASURED: route('render a scene') -> act 1.0 -> Rendering; route( +'distributed coordinator farm') -> choose (3 options); complete('material') -> material_info/... with signatures; +/skills manifest 434 caps + 702 methods over HTTP. +9 tests + integration. SERVICE.md documents the endpoints. + +## CROSS-ENGINE DISCOVERABILITY SWEEP: 12 domain homes so nothing hides [catalog-driven] + +Used the catalog as the gap-finder (Moose's suggestion) across ~19 families: posed the plain-English queries a user +would ask and flagged any returning only auto-docstring module hits or nothing. Found ~50 gaps -> registered 12 curated +DOMAIN homes: Rendering (path trace), Mesh editing (DCC), SDF & procedural geometry, Navigation & planning, Learning & +agents, Data analysis, Symbolic reasoning, Signal & spectral, Compression & codec, Video (temporal), Honesty & +measurement, Program & machine (VM) -- plus an Encoders home and particle/mass-spring aliases on Simulation. Expanded +tools/catalog_gaps.py to sweep all 19 families; it now reports 0 gaps. The few remaining module-name-wins (raymarch a +scene -> holographic_raymarch, particle system -> holographic_emitter) return the PRECISE correct module, not a gap. + +## SKILL LINT: no faculty an agent can't invoke from its docstring [+3 tests] + +Follow-up to the agentic layer: a skill card's summary is the first line of a method's docstring, so a missing/thin +docstring = a capability an agent can FIND but can't confidently CALL. BUILT tools/skill_lint.py (the invocation-quality +twin of tools/catalog_gaps.py): over all 703 public UnifiedMind methods it flags CRITICAL (no docstring at all) and +TERSE (summary < 5 words), plus an advisory NO_RETURN note (full docstring never hints at a return value). Deterministic, +stdlib-only; exit code = hard-gap count so CI can gate. + +RAN it: 4 CRITICAL, 0 terse. FILLED the 4 -- all core faculties that had only inline comments, no docstring: + * learn(x, label, modality=None) -- the base learning verb (perceive -> prototype + recall index; infers modality; + registers record fillers). Returns self. + * next_symbol(context, name=None) -- next-char prediction on a FLAT n-gram schema (needs + learn_sequence(hierarchical=False)); returns the next character. + * reinforce(state, action, reward, modality=None) -- the RL update into the decision brain; returns self. + * describe() -- human one-line summary of what the mind holds (prototypes/labels, recall index, brain, generators); + returns a string. (Distinct from describe_skill(name), the machine-readable card.) +Re-ran: 0 CRITICAL + 0 TERSE. Improved the NO_RETURN advisory to scan the FULL docstring (610 -> 278 notes) so it's a +real soft signal, not noise -- left as advisory, NOT force-fixed (many are legitimate mutators or describe the return +without the trigger words). +3 tests + an integration GUARD (test_skill_lint_no_invocation_gaps) so a future +undocumented faculty fails CI until it gets a one-line 'what it does + what it returns' docstring. + +## SKILL LINT extended to home EXAMPLES: no broken/undocumented references an agent copies [+1 test] + +An agent following a skill card's `example` often calls a MODULE-level function directly (e.g. "from +holographic_raymarch import sphere_trace"), so those need to resolve AND be documented. Extended tools/skill_lint.py +with audit_home_examples(): it parses every curated home's example for `from holographic_X import Y` / `holographic_X.Y(` +references and flags BROKEN (name/module doesn't resolve -> ImportError for the agent), NO DOC, and TERSE. + +RAN it over 77 referenced functions -> found 6: FOUR BROKEN references (worse than a missing doc -- the example itself +was wrong) and TWO missing docstrings: + * broken: holographic_ai.cleanup (it's Vocabulary.cleanup, not module-level) -> example now imports bind, bundle + + Vocabulary; holographic_distribute.reduce (real names reduce_sum/min/max/bundle) -> fixed; holographic_meshpoly. + extrude (extrude is extrude_face in holographic_meshverbs) -> fixed; holographic_uri.content_id (real: + address_from_content / make_key) -> fixed. + * no doc: holographic_sdf.box, holographic_sdf.sphere -> documented all six SDF primitive leaves (sphere/box/torus/ + cylinder/plane/menger) with one-line docstrings. +Re-ran: 0 method gaps + 0 example gaps (BROKEN+NODOC+TERSE all zero). The linter's report now prints both surfaces and +its exit code / TOTAL covers both; the integration guard asserts no broken references too, so a future home example that +names a non-existent function fails CI. Lesson kept: a discoverable capability with a WRONG example is worse than an +undocumented one -- the lint now catches both. + +## DESCRIBE A SCENE -> BUILD -> ADJUST NAMED OBJECTS -> RENDER / SIMULATE [+9 tests] + +Moose: describe a scene and have the system build it, then render/simulate it; or take an existing scene of NAMED +objects/materials and reference + adjust them semantically. Probed first: holographic_semantic already PARSES a +description (parse_description) and REALIZES + renders it (realize_scene, render_scene, and mind.render_scene_description +does text->image in one call), and has find_objects/batch_set. What was MISSING: a live, adjustable scene object with +named objects, a natural-language ADJUST flow, and any of it wired as one ergonomic thing. + +Found + fixed a real PARSER BUG while probing: "a big red metal sphere and a small blue glass box ON A SUNNY DAY" +parsed to ZERO objects -- a clause carrying both objects AND weather was classified wholly as environment, dropping the +objects. Fixed to HARVEST env keywords from any clause but only DROP a clause's tokens when it is purely environmental +(and guard "blue" as sky only when no object is present). Now the sunny-day scene yields both objects. + +BUILT holographic_scene_semantic.py -- SemanticScene: named, mutable objects + environment, with + * scene_from_description(text) / mind.build_scene(text) -- the one-call "describe it, engine builds it" entry. + * mind.semantic_scene(objects) -- wrap an EXISTING object list to adjust semantically. + * .adjust(command) -- 'make the sphere bigger', 'change the red box to metal', 'make everything glass'. parse_adjust + disambiguates cleanly: a colour/material/size is a SELECTOR when a shape word FOLLOWS it ('red box'), else the + CHANGE ('make the box red'); relative sizes ('bigger') are always a change; explicit_all ('everything') vs an + UNRESOLVED target ('the pyramid' -> no known shape) -> the latter is a safe NO-OP, never edits everything by accident. + * .set(ref, **fields), .get/.select(ref), .names(), .describe(). + * .render(camera=None) -- best-effort 3-D render (default pulled-back camera; fast single-pass or hyperreal path + tracer), applying the scene's sun/sky. .simulate(steps) -- a deliberately SIMPLE rigid gravity-drop (point mass per + object, ground at y=radius) so "or simulate it" is real end to end; richer media stay behind the Simulation home. + * .encode(mind) -- the scene as one content-addressable hypervector (encode_scene). +Wired mind.build_scene / semantic_scene; registered "Scene from description (semantic)" (77 homes); route('describe a +scene and build it') -> act. +9 tests + integration (describe->build->adjust->render->simulate->encode). + +## SKILL LINT closes the loop on mind.method example refs [Moose's follow-up] + +Extended audit_home_examples() to also parse `mind.method(` references in home examples (not just module imports) and +verify each names a real public UnifiedMind method. Caught one immediately: the Language learning home said +mind.learn_text(corpus) -- there is no learn_text on the mind (it's encoder.learn_text); the mind's corpus reader is +mind.read(corpus). Fixed the example + the does text. Now 105 references checked (module + mind), 0 broken. So EVERY +copy-paste target in a skill card -- module function OR mind method -- is guaranteed to resolve. + +## SCENE ADJUST: suggestions & clarifying questions instead of silent failure [+7 tests] + +Moose: when a request is unclear, offer suggestions/questions to clarify (like a person would) rather than just +failing. Applied that to SemanticScene, matching the route() 'choose' pattern. holographic_scene_semantic now has +interpret_command(scene, command): it DOES ITS BEST (resolves known synonyms via the SynonymResolver table -- +crimson->red, chrome->metal, shiny->mirror), then returns a report -- understood / read_as / matched / applied / +questions / suggestions / unknown. adjust() applies what it can AND stores the report in scene.feedback; interpret() +(alias suggest()) previews without applying; options() lists the palette (object names + colour/material/size words). +Also gave the CREATE side the same courtesy: scene_from_description passes a resolver (so 'a shiny crimson orb' -> +red mirror sphere) and attaches feedback when a description yields no objects or has unknown words. Honest boundary +kept: 'make the pyramid golden' changes nothing (no known 'pyramid') but now explains + suggests the scene's real +objects, rather than silently no-op'ing. difflib supplies 'did you mean' for near-typos; stdlib only. +7 tests. + +## CI + DOCS leverage the new capabilities (digestible / discoverable / usable) [+4 tests] + +Goal: make leCore easier to digest, discover, and build on -- as a standalone app and a library -- and have CI enforce it. + +NEW GENERATED DOC: capdoc.py -> CAPABILITIES.md, a plain-language MENU of what the engine does, grouped into ~14 themes +(Core, Discover, Memory, Geometry, Scenes, Simulation, Language, Learning, Data, Compression, Honesty, Navigation, +Service, More), each home with its one-line 'what + how to start' call and the words to find it by. Reads the live +catalog (no module imports), same old-school stdlib style as docgen.py/apiquickref.py, and writes NO timestamp so the +drift check is stable (apiquickref's date-in-file actually makes ITS drift check fragile -- noted, not touched). + +CI (ci.yml) gained three things beyond pytest: a DISCOVERABILITY gate (tools/catalog_gaps.py -- every capability a user +would ask for has a curated home; made it sys.exit(gaps)), an INVOCATION gate (tools/skill_lint.py -- every faculty has +an agent-usable docstring and every home example, module OR mind.method, resolves), and a CAPABILITIES.md drift check. +docs.yml now regenerates + commits CAPABILITIES.md alongside REFERENCE.md. + +README updated with digestible sections: a runtime-discovery pointer under 'What can it do?' (find_capability/suggest/ +route + CAPABILITIES.md), a 'Describe a scene and shape it in words' example, a 'Run it as a standalone HTTP service' +pointer (SERVICE.md), an expanded 'Learning more' (CAPABILITIES/API_QUICKREF/SERVICE), and a 'How the docs stay honest' +note explaining the generated docs + the two CI gates. So both a person and an agent can find the front door. +4 tests +(capdoc: generates, deterministic, no timestamp, every home placed). + +## PACKAGING HARDENED: the wheel now ships its runtime data (pip install actually works) [+5 tests] + +Moose asked if the package publishing is good to go for `pip install`. Probed it and found a REAL gap: setup.py uses +py_modules (flat top-level modules) with NO package_data, and build_package.sh copied ONLY holographic_*.py -- so the +wheel shipped with NO data. holographic_dictionary reads data/knowledge/dictionary.json.gz at runtime (the wired +lookup/word_taxonomy/learn_vocabulary faculties), so a pip-installed dictionary would FileNotFoundError. (heat also +reads material JSON but degrades gracefully to defaults.) + +FIX -- a small importable data package (the standard way to ship data with a flat layout): created lecore_data/ (a +package with __init__.py + file()/exists() resolvers) and MOVED the runtime-vendored data into it +(lecore_data/knowledge/dictionary.json.gz + manifest + WordNet license; lecore_data/definitions/.../*.json). `import +lecore_data` resolves the same from a clone AND a wheel. Updated the two consumers (holographic_dictionary, +holographic_heat) to resolve via lecore_data with a fallback to the old data/ path. Demo-only datasets (market npz/json) +stay in data/ and are deliberately NOT shipped. + +Wired the packaging: setup.py gains packages=['lecore_data'] + include_package_data + package_data globs; +build_package.sh copies lecore_data/ into staging and strips __pycache__; MANIFEST.in includes it in the sdist; +PACKAGING.md documents why it's a package. Hardened the RELEASE gate: package.yml's smoke test now installs the wheel, +runs from an isolated temp dir, and asserts lecore_data.exists(dictionary) AND holographic_dictionary.size()>100000 -- +so a wheel built without its data fails the release instead of reaching users. + +PROVEN end to end: built the wheel, pip-installed it into a CLEAN venv away from the repo -> import lecore, UnifiedMind, +holographic_dictionary (144,478 words), mind.lookup, mind.word_taxonomy, build_scene, render all work. Wheel + sdist +both contain lecore_data; 0 __pycache__ in the wheel; all 374 modules present. +5 packaging tests (guard the data +ships, the loaders find it, setup.py/build_package.sh declare it) so plain pytest catches a regression before a wheel +is ever built. leCore is now genuinely `pip install leos-core`-able. + +## SERVICE.md DRIFT CHECK: the endpoint list can't fall out of date [+6 tests] + +Moose: add a drift check for SERVICE.md, like the CAPABILITIES.md one. SERVICE.md is different from CAPABILITIES.md +though -- most of it is hand-written prose worth keeping (curl examples, security notes, job-lifecycle explanation); +only the ENDPOINT TABLE structurally rots when routes change. So rather than regenerate the whole file, servicedoc.py +CHECKS that the table's (method, path) set exactly matches the routes the live Service registers -- catching an added, +renamed, or removed endpoint that wasn't documented. Proven to catch both directions (undocumented route, stale row). + +First made the endpoints self-documenting: added concise one-line docstrings to the 13 service handlers that lacked +them (format 'What it does. Body: {...}. Returns: {...}.'), so all 23 endpoints now describe themselves in the code -- +a readable win on its own, and it lets servicedoc's `--print` emit a paste-ready fresh table from the live routes. + +servicedoc.py (stdlib only, imports Service just to read its route table, no socket opened): routes() (live routes + +parsed docstrings), doc_endpoints() (parse the table), check() -> (missing, stale), generated_table(), and a __main__ +that reports + exits non-zero on drift. Wired into ci.yml as the 'SERVICE.md documents every endpoint' gate (next to +the catalog/skill/capabilities gates). Added a note in SERVICE.md that the table is CI-checked, and folded it into the +README 'How the docs stay honest' paragraph. +6 tests. Now every public surface -- module reference, capability menu, +API quickref, AND the service endpoints -- is kept honest by CI. + +## BURIED-TECH AUDIT: nothing left unwired or lost [+4 tests] + +Moose: now that everything's cataloged/agentic, check nothing is unwired or buried. Ran tools/reachability_audit.py +(373 modules): 312 reachable via a UnifiedMind faculty, 7 declared negatives, 0 undiscoverable, but 55 "import-only" +(findable via catalog, not a mind faculty). Deepened it with a cross-reference -- does ANY other module/tour/catalog/ +service reference it? 55 -> WIRED INTO A PIPELINE (infra/home modules reached through facades: lightcache<-lightinghome +<-pipeline, coordinator<-farm/jobs/hardening, etc. -- fine). That left 6 truly referenced-by-nothing modules, each with +its own passing test + selftests (they WORK, just wired to nothing): + +MEASURED (not assumed) each against its likely twin: +- SUPERSEDED DUPLICATES (4): query_history->querytime, query_programs->queryprog, query_concurrency->querylock, + query_graph->querygraph. Same backlog items (P7-P12, PR1-PR6, B8, B10) reimplemented under cleaner names that got + the curated homes; the underscore originals were left behind. querytime/queryprog are clear supersets; querylock/ + querygraph cover theirs. Constitution = additive, don't delete working+tested code, don't wire a duplicate -> added + a loud "SUPERSEDED BY holographic_" banner to each docstring (still works, tests pass, reader is redirected). +- BURIED UNIQUE (2): workspace (the WS3-WS6 3-tier session MANAGER -- persistent DB + isolated transient per-session + scratch, make/switch/clear/reset/export/import/combine) is NOT the same as queryfolder (WS7 table folders); and + query_durable (B7 write-ahead Journal + point-in-time recover) is unique beyond the service's plain save/load. Real + capability, wired to nothing. Wired them the way their siblings are -- curated catalog HOMES ("Workspaces (durable DB + + transient sessions)", "Durability & crash recovery") -- so find_capability/suggest/route reach them. Catalog 77->79. + +Taught reachability_audit.py to recognize "superseded by" as a declared marker (like kept negatives), so the 4 twins +read as declared, not unexplained (import-only 55->51). Result, verified by an independent cross-reference: "still +buried: NONE -- clean". +4 guard tests (the 2 unique modules stay discoverable; the 4 duplicates keep their banner; and +a no-module-is-buried check so this can't silently regress). + +## WIRING RE-VERIFY + GALLERY/TEST TECH-STACK SWEEP (clean) + a permanent guard [+3 tests] + +Two-part request: (1) confirm pipelines are fully wired / nothing buried, then (2) sweep gallery demos + tests for +correct tech stack. + +(1) RE-VERIFIED the wiring audit end to end (the buried-module resolution from earlier this session was intact, not +lost to a rollback -- I checked because a NOTES entry claimed it done while a scratch cross-ref of mine suggested +otherwise; the scratch script was just wrong). Authoritative state: reachability_audit + test_buried_audit both green. +312 modules reachable via a UnifiedMind faculty; 4 carry a SUPERSEDED-BY banner (query_concurrency/history/programs/ +graph -> querylock/querytime/queryprog/querygraph) and read as 'declared' in the audit; 7 declared negatives; 51 +import-only but all infra/home modules reached through facades; 0 undiscoverable; the 2 genuinely-unique buried +modules (workspace, query_durable) already have curated homes. NOTHING is buried. + +(2) SWEEP of make_gallery.py + all test_*.py: the stack is correct, nothing to fix (measured, not assumed): + * No gallery demo or test (nor any other module) imports a superseded/buried module -- canonical twins used + everywhere; superseded modules appear ONLY in their own file + own test. + * No banned deps (torch/scipy/sklearn/tensorflow/keras/cv2) anywhere in tests or the gallery. + * CORE PURITY holds: import lecore + UnifiedMind + find_capability + build_scene all work with numpy ALONE + (PIL/matplotlib/nltk/scipy/sklearn/torch/cupy/numba all blocked) -- image_vault's PIL dep is isolated to the APP + layer (app.py/unified_app.py), not core. + * Determinism: every random/np.random use in tests is SEEDED. The one np.random.seed(424242) in test_integration is + DELIBERATE -- it scrambles the global RNG to PROVE the calibrated paths draw only from their own seeded RNG (the + bind_batch discipline). Read the context before 'fixing' it; left it alone. + * make_gallery.py drives only canonical render modules (gbuffer/raymarch/pathtrace/matlib/sdf/adaptive_sample, all + present); matplotlib is Agg-backend, plot-only. Optional-dep tests (8x nltk, 1x PIL) match house style (deps in + requirements.txt), consistent across the suite. + +Turned the sweep into a PERMANENT guard: test_techstack.py -- (a) core-purity check in a SUBPROCESS with every +optional/banned dep blocked (catches a future PIL/scipy/etc. leak into a core-reachable import), (b) no non-test module +imports a superseded twin (a duplicate can't creep back into a pipeline), (c) the 4 duplicates keep their banner. +3 +tests. The one-time sweep is now a regression gate, like catalog_gaps/skill_lint/servicedoc/buried_audit. + +## CMP1: composable TEXTURE MAP GRAPH (readable tree + compose-time type schema) [+11 tests] + +First item of the render-composability backlog. The gap: the pipeline is composable and the scene half-composable, +but textures/materials are flat -- a texture is an FPE-over-UV function, a material a 2-way blend. The backlog's +insight (the engine's own): a texture map, a layered material, a scene node, and a pipeline stage are four costumes +of ONE thing -- a typed tree of nodes where a node is a leaf or an op over typed child inputs, with a schema saying +which types go where. CMP1 builds the first costume (textures) on that shape. + +NEW holographic_texturegraph.py: Node/Const/FieldLeaf/Map -- a texture is a readable OBJECT TREE. A Map is an op +(mix/multiply/over/scale/add/remap/min/max) over TYPED inputs, each of which may be another Map, so graphs nest to +any depth. sample(uv) walks the tree (evaluate children, apply op). THE DISCIPLINE IS THE SCHEMA: OP_SCHEMA declares +each op's slots and the kinds each accepts {map|color|field|number}; Map.__init__ checks it at COMPOSE time, so a bad +graph (colour as a weight, missing input, unknown op) is refused up front with a clear message -- 'composable +CORRECTLY', not just composable. Leaves reuse the existing Texture sources (fbm/voronoi/synth via field_leaf) and +fieldhome; nothing new. Encoding is OPTIONAL and only where it earns its keep: to_expr lowers the tree to typed's +(op, child, ...) form and encode() -> one hypervector via typed.encode_tree, for caching a baked result by graph +identity or searching a library (structurally identical graphs -> identical code, cosine 1.000). Kept the object tree +as the source of truth -- do NOT force a deep tree into one vector (HRR capacity cliff), the backlog's loud boundary. + +Wired into UnifiedMind: texture_leaf (a const value or a named Texture field), texture_map (compose+validate a node), +sample_texture, encode_texture (uses the mind's dim/seed). Catalog home "Texture graph (composable maps)" (catalog +79->80); find_capability('compose a texture from noise and colors') reaches it; skill_lint clean (example resolves). +Integration test builds a nested mix->multiply graph through the mind, samples an rgb, encodes it (identical structure +-> cosine>0.99), and confirms the schema refuses a colour-as-weight at compose time. +11 tests (10 module + 1 +integration). Next in the backlog: CMP3 (multi-material by field), then CMP2 (layered materials + order schema). + +## RELEASE GUARD: tag-vs-setup.py version consistency check [+5 tests] + +Closed the loop on the PyPI release path (offered earlier, now built). Releases are cut by pushing a git tag (v0.2.0), +but the version actually published is whatever setup.py says -- so tagging v0.2.0 while setup.py still reads 0.1.0 would +publish the wrong number, and PyPI never lets you re-upload a version. tools/check_version.py (stdlib, AST-only -- reads +setup.py without executing it) prints the version, or with --expect X asserts a match (a leading 'v' from a git tag is +accepted). Wired into package.yml as a fast-fail step on tag pushes, BEFORE the build: +`python tools/check_version.py --expect "${{ github.ref_name }}"` -- a mismatch fails the release before anything is +published. No-op on branch pushes. Runnable locally too (check before you tag). PACKAGING.md's Versioning section +documents it. +5 tests (reads version, match passes, leading-v accepted, mismatch fails nonzero, bare prints). + +## CMP3: MULTI-MATERIAL blended/selected by per-point masks [+9 tests] + +Second render-composability item (sequenced before CMP2 -- cheapest, reuses blend + CMP1). Material.blend mixes TWO +materials by one scalar t (constant over the surface). CMP3 generalises to N materials each weighted by a MASK that +VARIES over the surface -- a bundle weighted by a field, exactly the substrate's own move. + +NEW holographic_multimaterial.py: MultiMaterial(materials, weights, mode, normalize). Each weight/mask is coerced via +CMP1's _coerce, so a mask IS a CMP1 texture graph (mind.texture_map), a raw field, or a constant -- CMP1 feeds CMP3 as +the backlog intended. sample(name, uv) = sum_i w_i(uv) * material_i.sample(name, uv) over the materials that HAVE that +channel (a missing channel contributes 0, like blend's 'present on one side blends toward zero'). Two modes: 'blend' +(soft weighted sum) and 'select' (hard argmax pick -- a material-ID / splat map, crisp boundaries). + +KEPT NEGATIVE (loud, and demonstrated in the selftest + tour): masks must PARTITION or brightness drifts. Default +normalizes weights to a partition of unity per point (w_i/sum_j w_j) with a uniform fallback where all masks ~0 (so a +point never goes black); normalize=False sums raw (shown drifting ~2x too bright). Also clamps negative mask weights. + +IMPORTANT MEASUREMENT NOTE: Material.sample is a COSINE readout (VectorFunctionEncoder.query = cosine(field, +encode(uv))) -- direction, scale-normalised. So two materials that differ only by a constant read IDENTICALLY; they +must differ in PATTERN. The tests therefore verify the blend FORMULA (== w0*A+w1*B to 1e-9) against two OPPOSITE albedo +ramps, which is encoder-agnostic and robust -- not an assumed absolute value. + +Wired into UnifiedMind: mind.multi_material(materials, weights, mode, normalize) -> MultiMaterial. Catalog home +"Multi-material (mask-blended)" (catalog 80->81); find_capability('paint rust onto metal with a mask') reaches it; +gates green (catalog_gaps + skill_lint). Integration test blends two materials by a CMP1 fbm mask through the mind and +pins the exact weighted-sum + partition-of-unity + select-picks-dominant. +9 tests (8 module + 1 integration). Next: +CMP2 (layered materials + a PlanShape order schema: base (stale, undocumented): +a documented flag that no longer exists, or a user-facing argparse flag SERVICE.md never mentions. _INTERNAL_FLAGS +({--selftest}) are exempt (internal/test-only); _DOC_ONLY_FLAGS ({--print}) excludes the doc-tooling flag SERVICE.md +mentions (servicedoc.py --print) so it isn't misread as a stale service flag. Wired into __main__ so the existing CI +gate (python servicedoc.py) now covers endpoints AND flags -- no ci.yml step added, just its comment + the SERVICE.md +note + the module docstring updated. Current state: 23 endpoints + 5 flags, all in sync. Proven to catch both +directions (stale doc flag, undocumented argparse flag). +4 tests. Every hand-maintained public surface is now +CI-guarded: module reference, capability menu, API quickref, and the service's endpoints AND launch flags. + +## CMP2: LAYERED MATERIALS with a layer-ORDER schema [+10 tests] + +Third render-composability item. Real surfaces are STACKS (base under diffuse under specular/reflection under +coat/clearcoat) and the ORDER matters -- a clearcoat sits ON the paint, never under it. CMP2 makes the order a SCHEMA +checked at compose time. + +NEW holographic_layeredmaterial.py: LAYER_RANK maps each kind to a tier (base 0, diffuse 1, specular/reflection 2, +coat/clearcoat 3) -- ONE readable table is the whole order schema. Layer(kind, Material, alpha) is one layer; its +coverage alpha is a number, a field, or a CMP1 texture graph (coerced via CMP1), so a coat can cover only part of a +surface. LayeredMaterial holds an ordered bottom-to-top list; add() REFUSES a layer whose tier is below the one under +it (a diffuse above a reflection, a base above a coat) with a clear message -- composable CORRECTLY. sample(channel, +uv) composites the layers that carry that channel from the bottom up: value = alpha*layer + (1-alpha)*below (the same +"over" Material.blend seeds, lifted to a stack); a layer without the channel is skipped (doesn't occlude what it +doesn't define). Optional encode() lowers the ordered stack (kinds + channels, in order) to a typed tree for +cache/search -- reordering changes the code. + +Chose a plain readable rank-table check over bending planshape to this: probed planshape and it's a holographic +plan ENCODER/DECODER (schema-as-decode-key), the wrong tool for a simple ordering rule. Readable-first; typed stays for +the encode-where-it-earns-it path. + +KEPT NEGATIVE (loud, in docstring + tour): ORDERING IS NOT ENERGY CONSERVATION. This fixes the STACKING (which layer +is above which + an over-composite of values); it does NOT do the radiometry of a true layered BRDF, where a coat +physically darkens/tints what's under it (Fresnel + absorption). Correct ordering shipped; the energy-conserving BRDF +is a separate, harder thing and is NOT claimed. Same cosine-readout measurement note as CMP3 (tests use opposite ramps ++ verify the over formula to 1e-9). + +Wired into UnifiedMind: mind.material_layer(kind, material, alpha) + mind.layered_material(layers). Catalog home +"Layered material (order schema)" (catalog 81->82); find_capability('put a clearcoat on top of paint') reaches it; +gates green. Integration test stacks base+coat with a CMP1 fbm coverage mask through the mind, pins the exact over +formula, and confirms the order schema refuses base-above-coat. +10 tests (9 module + 1 integration). Backlog: CMP1, +CMP3, CMP2 done; next CMP4 (type-correct scene binding surface<->mesh / volumetric<->volume + shared-definition +instancing on scenegraph), then CMP5 (pipeline orchestrates the graphs -- makes 'adaptive' reach down to maps/materials). + +## README EXAMPLES NOW RUN (and are CI-guarded) [+1 test] + +The README's headline code block was BROKEN -- it called mind.remember/encode/bind/unbind/cleanup, none of which exist +on UnifiedMind (only recall did). Since README.md is setup.py's long_description, that broken snippet is the first +thing a visitor to the PyPI page sees, and agents/users copy-paste it. Rewrote it against the REAL, measured API: +mind.learn(x,label) + mind.recall(x) -> ((label, description), score) for content recall; and the raw algebra via the +module functions (from holographic_ai import Vocabulary, bind, unbind; vocab.get(name); vocab.cleanup(noisy) -> +(name, sim)). Verified every line runs and recalls 'apple' / cleans up to 'filler'. Dropped the "method names are +illustrative" disclaimer -- they're real now. Guard: test_readme_examples.py extracts every ```python block, +concatenates them in reading order, and runs the whole thing in a subprocess (bash blocks ignored) -- so a README +example can never silently rot again. The scene block (build_scene/adjust/render/simulate/options) was already real and +runs too. +1 test (2.7s). + +## POLISH SWEEP: UX fixes found by using the render/composability flows as a user [+7 tests] + +Exercised the user-facing surfaces (build_scene->render, the CMP1-3 faculties, discovery, dictionary) looking for +rough edges. The semantic render pipeline is SOLID: 'a big red metal sphere on a green box' parses shapes/colours/ +materials + the 'on' relation correctly and renders the sphere sitting on the box with ground+shadows+sky (verified by +eye on server PNGs). Fixed the real UX issues found: + +- texture_leaf(value='red') used to throw a raw numpy 'could not convert string to float' -- now Const accepts a + COLOUR NAME (resolved via the scene system's COLORS, so the vocabulary matches everywhere), or gives a clear message + listing valid names. _coerce routes strings through Const, so named colours work anywhere a leaf does. +- MultiMaterial / Layer used to fail with a cryptic AttributeError deep in sample() when handed a non-Material -- now + they validate at COMPOSE time ('material 0 is not a Material (needs .channels and .sample)'), which is the backlog's + own 'validate at compose time, not render time' principle. +- field_leaf('perlin') now lists the available Texture sources (curl/fbm/synth/voronoi) instead of just naming the bad one. +- Added 'saturate' (clamp to [0,1]) and 'clamp' (to [lo,hi]) ops to CMP1 -- composition can push colours out of range + (fbm > 1, mix extrapolates, honest by design); saturate is the one-op fix a texture author reaches for. + +Non-issues confirmed (measured, not assumed): 'purple'/'shiny' adjust correctly (real colour / shiny->mirror); +suggest() returns a clean list of {name,does,call}; route('render a 3d scene') sensibly OFFERS both the semantic and +path-trace paths; lookup(missing_word) returns None (a defensible contract). Core purity still holds -- the new lazy +COLORS import is inside _named_color, so importing the mind stays NumPy-only. +7 tests pin all the fixes. + +## CMP4: type-correct scene BINDING + shared-definition INSTANCING (edit-once) [+11 tests] + +Fourth render-composability item. Two things a flat object list lacks: + +(1) TYPE-CORRECT BINDING. A material has a KIND (surface: paint/metal/glass; volumetric: fog/smoke/fire = participating +media) and geometry has a kind (surface mesh / volume). You may only bind like to like. Definition._bind checks it at +COMPOSE time (and on every edit), so smoke-on-a-mesh or paint-on-a-volume is refused with a clear message, not rendered +wrong. Reuses semantic._VOLUMETRIC as the single source of "what is volumetric". + +(2) SHARED-DEFINITION INSTANCING (edit-once). Definition = a named shared geometry+material unit; Instance = a +placement of ONE Definition through a transform, holding a REFERENCE (properties read through to the definition). +InstancedScene collects instances; set_material/set_geometry on the definition updates EVERY instance in one edit +(measured: repaint 'chair' once -> all 3 instances read 'glass'). An invalid repaint is refused AND leaves the +definition unchanged. + +Chose a plain readable rank/kind check over planshape again (same call as CMP2 -- planshape is a holographic plan +encoder, wrong tool for a simple type-match rule). Reuses scenegraph (SceneNode + flatten_scene, translation) for the +geometry view. + +KEPT NEGATIVE (loud, docstring + tour): sharing is edit-once at the GRAPH level; flatten_surface() is where instances +become concrete geometry -- it materialises each surface instance's shared mesh through its transform and merges into +ONE mesh (24 = 3x8 verts for 3 cube instances); volume instances are listed separately (not triangles). Edit-once +lives on the graph you flattened FROM, not the flattened mesh. + +Wired into UnifiedMind: mind.shared_definition(name, geometry, material, geometry_kind=None) + mind.instanced_scene(). +Catalog home "Instancing (shared definition + type-safe binding)" (catalog 82->83); find_capability('place the same +chair many times and recolor all at once') reaches it; gates green. Integration test places a shared def 3x through +the mind, edits once (all change), refuses smoke-on-mesh, flattens 2 instances to one mesh. +11 tests (10 module + 1 +integration). Backlog: CMP1,3,2,4 done; LAST is CMP5 (pipeline orchestrates the graphs -- bake a static texture graph +via the Cache home, resolve a layered material, bind the scene by schema, render; makes 'adaptive' reach down to +maps/materials). + +## CMP5: the PIPELINE composes the graphs (bake vs live) -- composability backlog COMPLETE [+8 tests] + +Last render-composability item. The pipeline already orders render/sim STAGES by needs/produces; CMP1-CMP4 gave the +graphs BELOW a render (texture maps, multi/layered materials, a type-checked instanced scene). CMP5 joins them: an +orchestrator that prepares those graphs into a render-ready scene as pipeline stages, adding the one decision +'adaptive' is really about at this level -- BAKE a static texture graph to a grid (O(1) lookup) vs SAMPLE it live. + +NEW holographic_rendergraph.py: BakedTexture (a 2-D texture graph evaluated to a grid once via CMP1's sample_grid, +read back by BILINEAR interpolation -- the 2-D twin of matbake's 3-D BakedField, same idea + same kept negative); +bake_texture(graph, res); resolve_texture(graph, bake='auto', static) -- the adaptive decision in one place (bake a +static map to amortise the tree walk over many hits; keep a changing map live so it isn't re-baked every frame; both +share .sample(uv) so downstream doesn't care which it got). RenderGraph is the orchestrator: add_texture(name, graph, +static) + set_scene(cmp4_scene); plan() reports bake-vs-live per texture + the scene bind WITH WHY (mirrors the render +pipeline's plan() -- see before you run); prepare() runs the stages and returns a PreparedScene (resolved textures + +one flattened surface mesh + volume instances aside). _stages() are real holographic_pipeline.Stage objects declaring +needs/produces, so this genuinely IS 'the pipeline composing the graphs'. + +KEPT NEGATIVE (loud): baking trades MEMORY for speed + INTERPOLATION error (blurs detail finer than a grid cell -- +raise res or keep sharp maps live); measured baked-vs-live match to 0.000 on a smooth fbm blend at res=128. Reuses +CMP1 (graph+sample_grid), CMP4 (bind+flatten), pipeline.Stage, and matbake for the 3-D material bake. + +Wired into UnifiedMind: mind.render_graph(res) + mind.bake_texture(graph, res). Catalog home "Render graph (bake vs +live)" (catalog 83->84); find_capability('bake a static texture for speed vs sample it live') reaches it; gates green. +Integration test runs the whole CMP1->CMP4->CMP5 chain through one mind (texture graph baked, dynamic kept live, scene +bound+flattened). +8 tests (7 module + 1 integration). + +*** RENDER COMPOSABILITY BACKLOG COMPLETE: CMP1 (texture map graph) + CMP2 (layered materials + order schema) + CMP3 +(multi-material by mask) + CMP4 (type-correct binding + shared-definition instancing) + CMP5 (pipeline bakes/binds the +graphs). One recursive, schema-checked composition -- a texture is a typed tree, a material is a typed ordered tree, a +scene is typed instances with transforms, a pipeline is a typed tree of stages -- five costumes, each readable, each +type-checked at compose time, each with its kept negative on the record. *** + +## PREVIEW: SEE what you composed -- texture swatch + material ball [+8 tests] + +Follow-on to the composability backlog (CMP1-5). Those build things you .sample(uv); the missing step was LOOKING at +them. holographic_preview.py: texture_image(graph) renders a CMP1 texture graph as a flat RGB swatch (colour graph -> +rgb, scalar -> greyscale, clamped to [0,1] for display); material_ball(material) renders a material on the classic +preview SPHERE, orthographic camera + one light, shaded with the SAME holographic_brdf.cook_torrance the real renderer +uses (so a preview matches a render), reading roughness/metallic off the material's channels and modulating a base +tint by an albedo channel if present. Works on a plain Material OR a CMP2/CMP3 layered/multi material (anything with +.sample(channel, uv) -- a _channel_names helper handles both .channels and .channel_names()). Both return (res,res,3) +float in [0,1]. The only per-pixel loop is sampling the material over the sphere's visible pixels; shading is +vectorised. Verified by eye on server PNGs: an orange/purple fbm swatch and a gold metallic ball with a roughness ramp +read correctly. + +Wired into UnifiedMind: mind.preview_texture(graph, res) + mind.preview_material(material, res, base_color). Catalog +home "Preview (swatch & material ball)" (catalog 84->85); find_capability('see what my composed material looks like on +a ball') reaches it; gates green. Integration test composes a CMP1 texture + a CMP2 layered material through the mind +and previews both. +8 tests (7 module + 1 integration). Closes the compose->see loop for the whole CMP1-5 stack. + +## TEXTURED SCENE RENDER: a composed texture/material painted onto a scene object in a FULL render [+6 tests] + +The capstone the preview work pointed at: take a CMP1 texture graph / CMP2-3 material and paint it onto an actual +scene object, rendering the whole scene -- so the composability stack drives a real 3-D image, not just a swatch/ball. + +NEW holographic_texturerender.py, render_textured(scene, textures, ...): reuses the engine's own machinery rather than +a new renderer -- realize the SemanticScene to SDFs, MARCH the union with sphere_trace (hit point + which object via +_UnionSDF.ids), turn each hit's 3-D surface point into a UV (spherical map on a _SphereSDF, planar/dominant-face on a +_BoxSDF), sample the texture the user attached to that object at its UV -> albedo, shade with the SAME +holographic_brdf.cook_torrance the renderer uses + a directional light + a hard shadow (march toward the light) + a +little ambient, sky gradient + ground behind. A CMP1 colour graph paints a DIFFUSE albedo (metal kills the diffuse +term + reads dark under one light, so a colour texture is shaded diffuse on purpose); a Material contributes +roughness/metallic + tints a base by an albedo channel. objects without an entry keep their scene colour. + +VERIFIED (viewer glitched on the marched PNGs, so verified by MEASUREMENT): 23,436 sphere pixels; within-sphere colour +std [0.083, 0.067, 0.068] with R in 0.09-0.44 and B in 0.08-0.38 -- the red<->cyan texture VARIES across the surface, +i.e. it genuinely WRAPS via UV mapping, not a flat recolour. Sky/ground/sphere regions all read correct colours. +Needed a lighting fix: cook_torrance carries 1/pi (dim under a unit light) -> a light-intensity ~pi + ambient, and +colour textures forced diffuse, so the pattern reads brightly. + +KEPT NEGATIVE (loud, docstring + tour): textbook UV mapping (a seam + pole pinch on a sphere, face seams on a box, no +triplanar blend); a single hard light (no soft shadows / GI -- the path tracer is the tool for that). A faithful, +readable BRIDGE, not a production shader. + +Wired into UnifiedMind: mind.render_textured(scene, textures, width, height). Catalog home "Textured scene render +(composed maps on objects)" (catalog 85->86); find_capability('paint my composed texture onto the sphere and render +it') reaches it; gates green. Integration test composes a texture through the mind, paints it on a scene sphere, renders, +and asserts the per-UV colour variation (the wrap) holds end to end. +6 tests (5 module + 1 integration). Closes the +loop: compose (CMP1-5) -> preview (swatch/ball) -> RENDER onto the scene. + +## SCENE FLOW: named objects + textures you attach by talking [+8 tests] + +Two additions to the describe-a-scene flow so a user can name what they build and paint it in plain English, and the +normal scene.render() shows the paint. + +NAMED OBJECTS. Each object dict gained a `label` (a user nickname) and a `texture` field. New SemanticScene methods: +name(reference, label) (unique -- reusing a label moves it), rename(old, new), labels(). select() and +interpret_command are now LABEL-AWARE: a nickname mentioned in a command is matched (whole-word) BEFORE attribute +parsing and wins, and labels are added to the known vocabulary so 'hero' isn't flagged unknown. interpret_command now +returns matched_idx, and adjust() applies changes to those indices (so 'make hero bigger' works). names() shows the +label when set; describe() shows 'label = description'. adjust() also parses naming commands: 'call/name