Skip to content

Commit 6bdc032

Browse files
authored
Merge pull request #64 from AdaWorldAPI/claude/q2-fma-v3-bake
/body: full-resolution FMA anatomy viewer (V3 substrate, F16 wire, x-ray, search, server LOD)
2 parents 4fecece + 7418218 commit 6bdc032

24 files changed

Lines changed: 2278 additions & 40 deletions

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,12 @@ CLAUDE.local.md
5151

5252
# Visual brainstorming companion scratch files
5353
.superpowers/
54+
55+
# FMA body.soa bake scratch (regenerated from uploads; do not commit)
56+
scratch-fma/
57+
crates/osint-bake/tools/__pycache__/
58+
59+
# big FMA body wire — lives in GitHub Releases, never in git
60+
cockpit/public/body.soa
61+
cockpit/public/body.soa.gz
62+
scratch-fma/

Cargo.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,13 @@ path = "../lance-graph/crates/lance-graph-contract"
222222
# u16). family(u16) is the deterministic basin (round→anchor), NOT a category and
223223
# NOT a Louvain cluster — location is permanent via the HHTL, the basin is an
224224
# interface the node adapts to.
225+
# NOTE: guid-v3-tail is NOT enabled workspace-wide. Only `osint-bake` mints on the
226+
# V3 cascade tail (the FMA bake), so it requests `guid-v3-tail` on its OWN dep
227+
# (crates/osint-bake/Cargo.toml). Enabling it here forced EVERY member — including
228+
# cockpit-server — to require a lance-graph-contract carrying the feature, which
229+
# broke the Railway cockpit build against the deliberately-pinned LANCE_GRAPH_REF
230+
# (Dockerfile COUNT_FUSE lockstep). cockpit-server uses no V3 minting; it needs
231+
# only guid-v2-tail (the OSINT/Gotham v2 leaf·family·identity tail).
225232
features = ["guid-v2-tail"]
226233

227234
# OGAR Active-Record activation crate — the real `impl lance_graph_contract::
@@ -237,6 +244,17 @@ path = "../lance-graph/crates/causal-edge"
237244
[workspace.dependencies.graph-flow]
238245
path = "./crates/stubs/graph-flow"
239246

247+
# The REAL AdaWorldAPI/ndarray fork, consumed DIRECTLY — the SAME local checkout
248+
# lance-graph compiles (`../ndarray`), so the whole binary unifies on one fork.
249+
# No wrapper crate: when everything compiles into a single binary from local
250+
# source, wrapping ndarray behind another crate buys nothing — it only hides
251+
# `ndarray::simd` / `ndarray::hpc` (the AVX-512→AVX2→scalar polyfill, compile-time
252+
# `target-cpu=x86-64-v4` + runtime `simd_caps()`) behind a pointless indirection.
253+
[workspace.dependencies.ndarray]
254+
path = "../ndarray"
255+
default-features = false
256+
features = ["std", "hpc-extras"]
257+
240258
[workspace.dependencies.q2-ndarray]
241259
path = "./crates/stubs/q2-ndarray"
242260

Dockerfile

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,28 +50,45 @@ COPY . /build/q2
5050
# so include_dir! can embed it at compile time
5151
COPY --from=frontend /build/dist/ /build/q2/cockpit/dist/
5252

53+
# Pull the big FMA body wire (BSO2) from the q2 release into dist/ so include_dir!
54+
# embeds it and the server serves it SAME-ORIGIN at /body.soa.gz. The browser cannot
55+
# fetch the release URL directly (github.com/.../releases/download sends no CORS
56+
# header on its redirect → "TypeError: Failed to fetch"), so /body fetches the
57+
# same-origin copy. The asset stays in the release (downloaded at build), never git.
58+
RUN curl -fSL https://github.com/AdaWorldAPI/q2/releases/download/fma-body-soa-v3-v1/body.soa.gz \
59+
-o /build/q2/cockpit/dist/body.soa.gz \
60+
&& ls -lh /build/q2/cockpit/dist/body.soa.gz
61+
5362
# Sibling deps — clone from GitHub
5463
# graph-flow stub is local (crates/stubs/graph-flow), no rs-graph-llm needed
5564
#
56-
# lance-graph is PINNED to an explicit commit (NOT `--depth 1 main`) for two
57-
# reasons:
58-
# 1. Cache-bust. A `--depth 1 main` clone lives in its own Docker layer that
59-
# an empty/unrelated q2 commit does NOT invalidate, so Railway reuses a
60-
# STALE lance-graph from an earlier build. Bumping this SHA changes the
61-
# RUN and forces a fresh clone.
62-
# 2. COUNT_FUSE lockstep. lance-graph-ogar compile-asserts (E0080 on mismatch)
63-
# that lance_graph_contract::ogar_codebook::CODEBOOK.len() ==
64-
# ogar_vocab::class_ids::ALL.len(). q2's Cargo.lock pins ogar-vocab to a
65-
# fixed OGAR SHA (302c284 = 43 concepts); the lance-graph clone MUST carry
66-
# the matching 43-concept mirror. 36059ce0 is the #595 merge (ogar_codebook
67-
# synced to 43) — the matched pair of the ogar-vocab pin.
68-
# WHEN OGAR MINTS CONCEPTS: bump ogar-vocab in q2's Cargo.lock AND this SHA
69-
# together (after the lance-graph mirror lands), or the fuse trips again.
70-
ARG LANCE_GRAPH_REF=36059ce0
71-
RUN git clone https://github.com/AdaWorldAPI/lance-graph.git \
72-
&& git -C lance-graph checkout "${LANCE_GRAPH_REF}" \
73-
&& git clone --depth 1 https://github.com/AdaWorldAPI/ndarray.git \
74-
&& git clone --depth 1 https://github.com/AdaWorldAPI/neo4j-rs.git
65+
# lance-graph + ndarray are cloned at their BRANCH HEAD (latest) — NOT a pinned,
66+
# stale SHA. The repos at their tips are mutually consistent, so "use the latest of
67+
# everything" is the rule: a pinned-old lance-graph (36059ce0) is exactly what
68+
# lacked `guid-v3-tail` and broke the build. The `COPY . /build/q2` above changes on
69+
# every q2 commit, invalidating this RUN layer too, so each build re-clones fresh
70+
# (no stale-cache problem the old pin was guarding against).
71+
#
72+
# Sibling checkouts the path deps resolve against:
73+
# /build/lance-graph → lance-graph @ main HEAD — carries guid-v2-tail +
74+
# guid-v3-tail and the 65-concept ogar_codebook mirror.
75+
# /build/ndarray → the REAL AdaWorldAPI/ndarray fork, consumed by BOTH
76+
# lance-graph (../../../ndarray) AND q2-ndarray
77+
# (../../../../ndarray). `--depth 1` WITHOUT
78+
# --recurse-submodules: ndarray's workspace `exclude`s
79+
# crates/burn, so the burn submodule (AdaWorldAPI/burn.git)
80+
# is never needed — leaving it unfetched is correct.
81+
#
82+
# COUNT_FUSE: lance-graph-ogar asserts (E0080 on mismatch)
83+
# CODEBOOK.len() == ogar_vocab::class_ids::ALL.len(). Both move together at HEAD —
84+
# lance-graph main's 65-concept codebook mirror matches OGAR main's 65-concept vocab;
85+
# q2's Cargo.lock pins ogar-vocab to OGAR main HEAD (a1fb170). Always bump the two
86+
# repos' HEADs together, never one alone.
87+
#
88+
# neo4j-rs is intentionally NOT cloned — a discarded Neo4j-GUI experiment referenced
89+
# by no manifest; the only neo4j path is the opt-in `neo4j-fallback` (crates.io neo4rs).
90+
RUN git clone --depth 1 https://github.com/AdaWorldAPI/lance-graph.git \
91+
&& git clone --depth 1 https://github.com/AdaWorldAPI/ndarray.git
7592

7693
# CPU baseline: x86-64-v4 (the 4th microarch level — AVX-512F/BW/CD/DQ/VL on top
7794
# of v3's AVX2+FMA). This is the compile FLOOR; it flips on `target_feature =
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# /body — server-side HHTL LOD + helix + slicer-fill (option 2)
2+
3+
## Overview
4+
5+
`/body` must render the full FMA body as **filled polygons** (slicer-style infill,
6+
per material — tubes/vessels/solids), addressed on the **V3 substrate**
7+
(`classid 0x1000_0A01`, the `(part_of:is_a)` 8:8 cascade), with **LOD** driven by
8+
the HHTL depth-cascade. The earlier `/body` was wrong on every axis: raw-OBJ hollow
9+
shells, no fill, no LOD, no helix, V3 key mis-encoded as `(depth:is_a)`, renderer
10+
ignoring `classid`.
11+
12+
**Decision (operator, 2026-06-27): option 2 — compute server-side.** The HHTL LOD
13+
(`depth_cascade`), helix-3-byte, and slicer-fill run in `cockpit-server` (x86, full
14+
`F32x16` SIMD), streaming LOD-selected geometry to a thin three.js viewer. Rationale:
15+
ndarray's **wasm** SIMD backend (`simd_wasm.rs`) is an un-wired stub — `F32x16`
16+
falls back to scalar on wasm, ~16× too slow for per-frame client-side LOD. Native is
17+
fully polyfilled (AVX-512/AVX2/NEON), so the cascade belongs server-side. (Option 1 —
18+
complete the wasm `F32x16` v128 backend — is the alternative, deferred.)
19+
20+
`splat3d` is a gaussian raster (the rejected "confetti" path); we use ONLY its
21+
renderer-agnostic `depth_cascade` (LOD block-preselection) + `helix_orient`, and draw
22+
**polygons**, never gaussians.
23+
24+
## Foundation — DONE (verified)
25+
26+
`scratch-fma/lodprobe` (standalone, builds against ndarray `features=["std","splat3d"]`):
27+
body.spm1 → per-concept `BlockBounds{center,radius}``cascade_blocks(camera, …)`.
28+
Verified monotonic LOD: near ⇒ 1513/1658 `ProjectExact`; far ⇒ 1446/1658 `KeepCoarse`.
29+
API pinned: `depth_cascade::{BlockBounds, DepthCascadeBudget, HhtlAction(Reject/
30+
KeepCoarse/Refine/ProjectExact/RenderExact), cascade_blocks}`, `project::Camera`.
31+
32+
## Work items
33+
34+
### Phase A — V3 substrate correctness (independent of LOD)
35+
- [ ] Bake the cascade as **6×(8:8) `(part_of:is_a)`** tiles: walk BOTH
36+
`partof_inclusion_relation_list.txt` AND `isa_inclusion_relation_list.txt`; each
37+
tier byte-pair = `(part_of_rank << 8) | is_a_rank`; identity tier too. (Current bake
38+
packs `(depth:is_a)` and never walked part_of — wrong.)
39+
- [ ] `body.rs`: emit the 6 tiers directly from the (part_of,is_a) pair arrays; drop
40+
the `mixin_for_depth` hack.
41+
- [ ] Renderer: dispatch on `classid` — assert `0x1000_xxxx` (V3), decode the
42+
`(part_of:is_a)` tile per node, use it (group/colour/pick by the two axes).
43+
44+
### Phase B — multi-LOD geometry (the pyramid the cascade selects from)
45+
- [ ] Per concept, bake a decimation pyramid: L0 full-res (ProjectExact), L1/L2
46+
vertex-cluster-decimated (KeepCoarse). Store offsets per (concept, level).
47+
- [ ] BlockBounds table (centroid + radius) per concept, baked alongside.
48+
49+
### Phase C — slicer-fill + helix (the "3D printing slicer" infill)
50+
- [ ] Per solid material (tube/vessel/organ), generate infill geometry inside the
51+
shell (slicer-style), placed via HHTL tile coords + `helix_orient` 3-byte → exact
52+
location. Tubes get tubular infill; solids get volumetric.
53+
- [ ] Material-prototype texture per layer (tube/vessel/bone/…).
54+
55+
### Phase D — server endpoint + streaming viewer
56+
- [ ] `cockpit-server`: dep ndarray `features=["std","splat3d"]`; `/api/body/lod`
57+
(POST camera {view,fx,fy,w,h}) → `cascade_blocks` → assemble selected (concept,LOD)
58+
blocks → SPM1 stream.
59+
- [ ] `BodyV3.tsx`: thin — throttled orbit posts the camera; swap the streamed mesh.
60+
Drop the full 168 MB client fetch.
61+
62+
## LOCKED design (operator review, 2026-06-27) — supersedes the BSO1 AoS bake
63+
64+
The wire is **SoA columns** (`MultiLaneColumn`, 64-byte aligned, `Arc<[u8]>`),
65+
joined by ONE SoA row identity. **Store identities/indices, never raw values;
66+
ClassView dereferences.** Three separate 16-byte GUID columns (cheap: 16 B ×
67+
100k = 1.6 MB; × 396k surfels = 6.4 MB — separation beats bit-packing):
68+
69+
| column | 16-B GUID / value | content | resolves via |
70+
|---|---|---|---|
71+
| **address** | `classid 0x1000` + `(part_of:is_a)` **8:8** cascade + identity tier | the node key (unique; routable prefix) | ClassView / registry |
72+
| **location** | `XYZ` standard 3D | position — GPU/slicer native; Z = slice, X·Y = in-slice 256² grid (256=4⁴ hierarchical) | direct |
73+
| **helix** | **2 helices** + reserved | helix-pos (dir from origin 0,0,0, 3 B) · helix-normal (self orientation, 3 B) · depth derivable by trig from XYZ | direct decode |
74+
| material | **codebook index** | Doppler flow class (low-res artery / high-res artery / portal / hepatic-vein / caval) | ClassView → material prototype |
75+
| label | **codebook index** | never raw text | ClassView → text + synonyms |
76+
| edges | **SoA-row refs** | part_of parent / branches / supplies / synonym alias | ClassView |
77+
78+
Rules that fell out of review:
79+
- **Collusion = a location collision = same geometry ⇒ a ClassView resolution,
80+
not a bug.** (`celiac trunk ≡ celiac artery` = same lumen → ClassView aliases;
81+
the 3 celiac branches have distinct XYZ → distinct, linked as children.)
82+
Bilateral pairs are unique by x-sign for free.
83+
- **Relationships reference the SoA row (linked identity), never embed a
84+
neighbour's location/helix.** Edge says *who*; row says *where/what/oriented*.
85+
- **64k⁶ = (256³)⁴** — same space; XYZ-bytes factoring is the slicer-native one.
86+
- **Tubes:** normal is radial from the centerline ⇒ helix-normal derivable by trig
87+
from XYZ + the part_of branch-tree centerline; slicer fills cylindrically
88+
(depth-along-axis · helix-angle · radius). Explicit helix bytes only for
89+
non-radial surfaces (sheets/capsules).
90+
- **Material fill** densifies the *surface* (slicer-style), per Doppler class.
91+
- **Render:** Gouraud shading; **bgz17/Base17 (#17) palette drives the alpha /
92+
transparency** channel (17 levels). Keep **6+ M polygons** (NO decimation to
93+
1.6 M yet — LOD pyramid is later).
94+
- compute server-side (ndarray native SIMD); deno_core/V8 is the *document* JS
95+
engine, never in the 3D path.
96+
97+
## Shipped increments (2026-06-28)
98+
99+
### Vessel "inflatable tube" fix — `fill_body_soa.py`
100+
The slicer-fill cores ballooned where vessels curve: the radius was the
101+
perpendicular distance from the **global** PCA axis, so a point on a bend sits
102+
far off the straight axis → radius inflates. Fixed: bin the points along the
103+
axis, then derive each ring from its **own bin** — centroid = bin's local centre
104+
(follows the curve), radius = **median** perpendicular distance from *that* bin
105+
centroid (robust to outliers), clamped to an **absolute** `[RMIN, RMAX]`
106+
diameter boundary (`RMAX=0.020` ≈ 34 mm dia, covers the aorta, kills balloons).
107+
662 vessels → +71,872 core verts / +133,152 tris.
108+
109+
### Half-precision positions — the "A" brick
110+
> **SUPERSEDED to F16 (BSO2 ver 5).** BF16 (ver 4) was tried first and **rejected**:
111+
> its 7-bit mantissa gave a ~3 mm step near the head (y≈0.85) → a visible staircase
112+
> (Treppeneffekt) on the eye/brain. Shipped format is **F16 / IEEE half (ver 5)**
113+
> same 6 B/vertex, 10-bit mantissa, ~0.2 mm (measured 0.21 mm max over the wire), no
114+
> staircase. Bake uses ndarray's `F16::from_f32`; renderer widens via a 64K half→f32
115+
> LUT. `BodyV3.tsx` reads ver 3 (f32) / 4 (BF16) / 5 (F16). gz ≈ 63 MB (vs f32's 80).
116+
> The BF16 description below is retained for history.
117+
118+
Per-vertex `pos` column was **BF16** (3× u16 LE = 6 B/vertex), half of ver 3's
119+
12 B f32. Conversion via ndarray's sanctioned RNE batch path
120+
(`f32_to_bf16_batch_rne`) on the native bake host (AVX-512/AMX). The renderer
121+
widens back to f32 client-side (`bits << 16` — BF16 is the top 16 bits of f32, so
122+
the widening is exact). Round-trip ≈ 1.4 mm — which turned out to be visible on
123+
small smooth structures, hence the F16 upgrade above. Asset in release
124+
`fma-body-soa-v3-v1` (Dockerfile pulls it same-origin).
125+
126+
### "B": server-side HHTL-O(1) LOD endpoint — WIRED (de-risked)
127+
cockpit-server can't build in-sandbox (quarto-core→runtimelib is a proxy-blocked
128+
git dep), so B is a blind deploy — de-risked three ways:
129+
1. **Verified core, standalone:** `scratch-fma/bodylod` builds + runs here against
130+
ndarray-only. `build_blocks(wire)` → per-concept `BlockBounds`, `concept_actions`
131+
`cascade_blocks` (HHTL HEEL→HIP→TWIG→LEAF, O(concepts≈1658), the O(1)
132+
reference). Monotonic LOD on the real BF16 wire: near 1521 ProjectExact / 137
133+
KeepCoarse → far 211 / 1447. The cockpit-server handler reuses this exact logic.
134+
2. **Tiny embedded asset, not the geometry:** `soabake` bakes `body.blocks`
135+
(1658×16 B = 26 KB: centroid + radius per concept, in the renderer's DISPLAY
136+
space so the client posts its three.js camera directly). cockpit-server
137+
`include_bytes!`s it — no 57 MB startup gunzip, no feature gate.
138+
3. **Opt-in client, default OFF:** `BodyV3.tsx` keeps the full render; a "server
139+
LOD" toggle (default off) posts the throttled camera to `/api/body/lod`, writes
140+
the per-concept `HhtlAction` bytes into a 1658-px R8 `DataTexture`, and the
141+
frag shader discards Reject concepts (gated by `uLodOn`). If the endpoint 404s
142+
(old deploy) or errors, it silently falls back to the full render. So a wrong
143+
cull (camera-space math is unverifiable here) only ever shows when the user
144+
flips the toggle — never by default.
145+
146+
Files: `crates/cockpit-server/src/body_lod.rs` (+ route in `main.rs`, `splat3d`
147+
feature in `Cargo.toml`, `assets/body.blocks`); `cockpit/src/BodyV3.tsx`.
148+
**Deferred (Phase B pyramid):** with single-LOD geometry the cascade only
149+
distinguishes show/cull, so the win is frustum-culling whole concepts when zoomed
150+
in; switching KeepCoarse → a decimated mesh needs the L1/L2 decimation pyramid.
151+
152+
## Constraints
153+
- Big baked assets (LOD pyramid, fill) → GitHub Releases (q2 `fma-body-soa-v3-*`),
154+
never git. `cockpit/public/body.soa*` gitignored.
155+
- q2 workspace cargo can't build in-sandbox (proxy-blocked `runtimed` git dep);
156+
ndarray-only crates verify standalone; the server build runs on deploy.
157+
- No model identifier in any committed artifact.

0 commit comments

Comments
 (0)