From f9a3ad69c220a60305e6e6cec16d42250643ef3c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 10:26:20 +0000 Subject: [PATCH 1/3] bake: classify connective tissue (ligaments/tendons/membranes) into layer 7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The organ view showed tan limb-shaped strays floating in the lower body — a foot, forearm/leg "bone shafts", and detached pieces below the abdomen. They weren't organs: they were connective tissue misfiled as organ. Root cause: FMA's is_a tree files ligaments/tendons/membranes under /viscera/solid_organ/ligament_organ and /membrane_organ. tissue_of walks that chain and hits the "viscus" TYPEKEY (→ layer 3 organ) before recognising them as connective. The UI's connective layer (7) was defined but NO tissue type ever mapped to it — it was a dead compartment end to end. So interosseous membrane of leg/forearm, calcaneal tendon, long plantar ligament, stylohyoid/ thyrohyoid/vocal ligaments, retinacula, iliotibial tract, tarsal plates, and the superior-oblique trochlea all rendered as organ-layer strays. Fix: - bake_torso_splat.py: a "connective" TYPEKEY (ligament organ / membrane organ / tendon / aponeurosis / fascia / retinaculum), placed after muscle and before the organ-group keys so it wins over viscus. Added connective to TISSUE_RGB (ivory 224,219,204), TISSUE_OPACITY (0.60), TISSUE_CONTAINERS, SYSTEM_OF. - soabake + helixbake layer_of (scratch emitters): "connective" => 7. - bake_body_v3.py + audit_body_semantics.py LAYER_OF mirrors: connective => 7. - audit_body_semantics.py: new QA-3 assertion that interosseous membrane / calcaneal tendon / long plantar ligament / iliotibial tract are layer 7 (regression guard). Fixed a latent bug it exposed — the eyeball check's bare "retina" matched "retinaculum"; tightened to \bretina\b. Validated against the FMA is_a tree before re-emit: exactly 39 concepts move to connective (21 from organ, 16 from skin, 2 from muscle); zero real organs move. New layer histogram: organ 151→130, connective 0→39. The lowest organ concepts are now real pelvic organs (testis/prostate/bladder) — no foot, no limb bones. Re-emitted as the stamped 20260629c artifacts (geometry byte-identical to 20260629b — only the per-concept tissue/layer changed, so no mesh re-read or slicer-fill needed; body.blocks is unchanged, the server LOD asset stays valid). Dockerfile + manifest bumped to 20260629c. QA-3 audit passes. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01RhpwkHGgia2TuDFvdnuQdE --- Dockerfile | 8 ++++---- cockpit/public/body.manifest.json | 4 ++-- crates/osint-bake/tools/audit_body_semantics.py | 10 +++++++++- crates/osint-bake/tools/bake_body_v3.py | 1 + crates/osint-bake/tools/bake_torso_splat.py | 16 +++++++++++++--- 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7bc46efd2..fdb5d9778 100644 --- a/Dockerfile +++ b/Dockerfile @@ -58,16 +58,16 @@ COPY --from=frontend /build/dist/ /build/q2/cockpit/dist/ # 20260629b re-bake: teeth → skeleton + per-vessel diameter boundary (no stray fat # branches). Pulled under its stamped name, served same-origin AS body.soa.gz so /body # picks it up; the old body.soa.gz stays in the release untouched. -RUN curl -fSL https://github.com/AdaWorldAPI/q2/releases/download/fma-body-soa-v3-v1/body.20260629b.soa.gz \ +RUN curl -fSL https://github.com/AdaWorldAPI/q2/releases/download/fma-body-soa-v3-v1/body.20260629c.soa.gz \ -o /build/q2/cockpit/dist/body.soa.gz \ && ls -lh /build/q2/cockpit/dist/body.soa.gz # Same for the /helix wire: one SoA (BSO2 ver 6) = F16 pos + a canonical Signed360 # NORMAL column in the same struct-of-arrays. Same-origin for the same CORS reason; # named by cockpit/public/body.manifest.json (helix_latest). Stays in the release. -RUN curl -fSL https://github.com/AdaWorldAPI/q2/releases/download/fma-body-soa-v3-v1/body.20260629b.v6helix.soa.gz \ - -o /build/q2/cockpit/dist/body.20260629b.v6helix.soa.gz \ - && ls -lh /build/q2/cockpit/dist/body.20260629b.v6helix.soa.gz +RUN curl -fSL https://github.com/AdaWorldAPI/q2/releases/download/fma-body-soa-v3-v1/body.20260629c.v6helix.soa.gz \ + -o /build/q2/cockpit/dist/body.20260629c.v6helix.soa.gz \ + && ls -lh /build/q2/cockpit/dist/body.20260629c.v6helix.soa.gz # Sibling deps — clone from GitHub # graph-flow stub is local (crates/stubs/graph-flow), no rs-graph-llm needed diff --git a/cockpit/public/body.manifest.json b/cockpit/public/body.manifest.json index 6d0d11398..f7157a5bb 100644 --- a/cockpit/public/body.manifest.json +++ b/cockpit/public/body.manifest.json @@ -1,5 +1,5 @@ { - "helix_latest": "body.20260629b.v6helix.soa.gz", - "note": "20260629b re-bake from soa_v2: teeth reclassified into the skeleton (layer 4) + per-vessel slicer-fill diameter boundary (no stray fat branches at bends). BSO2 ver 6 = F16 positions + Signed360 NORMAL column + HXFL floor trailer; helixbake (real lance-graph::helix::encode_signed). Decode: rim r=sinθ -> int8 normal at load, Gouraud per-vertex shading. Published to fma-body-soa-v3-v1; Dockerfile pulls same-origin.", + "helix_latest": "body.20260629c.v6helix.soa.gz", + "note": "20260629c re-emit from soa_v2 (geometry identical to 20260629b): 39 connective structures (ligaments / tendons / interosseous membranes / fascia / retinacula / iliotibial tract) reclassified out of the ORGAN and SKIN layers into the now-live CONNECTIVE layer 7 — they were FMA-filed under /viscera/solid_organ/ligament_organ, so the is_a walk tagged them viscus->organ and they floated in the organ view as tan limb-shaped strays (interosseous membrane of leg/forearm, calcaneal tendon, long plantar ligament). Carries the 20260629b fixes (teeth->skeleton, per-vessel slicer-fill diameter). BSO2 ver 6 = F16 pos + Signed360 NORMAL + HXFL trailer; Gouraud per-vertex shading. Published to fma-body-soa-v3-v1; Dockerfile pulls same-origin.", "verts": 4283525 } diff --git a/crates/osint-bake/tools/audit_body_semantics.py b/crates/osint-bake/tools/audit_body_semantics.py index 27e854678..124db93bf 100644 --- a/crates/osint-bake/tools/audit_body_semantics.py +++ b/crates/osint-bake/tools/audit_body_semantics.py @@ -31,6 +31,7 @@ "skin": 1, "flesh": 1, "muscle": 2, "heart": 3, "lung": 3, "liver": 3, "kidney": 3, "gi": 3, "gland": 3, "viscus": 3, "bone": 4, "cartilage": 4, "artery": 5, "vein": 5, "vessel": 5, "nerve": 6, + "connective": 7, } LAYER_NAME = {1: "skin", 2: "muscle", 3: "organ", 4: "skeleton", 5: "vessel", 6: "nervous", 7: "connective", 8: "other"} @@ -173,7 +174,9 @@ def assert_layer(label_pat, want_layers, exclude=None): # liver parenchyma → organ (exclude the true hepatic/portal vessels) assert_layer(r"hepatovenous segment|caudate lobe of liver", {3}) # eyeball structures → organ (skin-layer flesh ok too); must NOT be vessel - assert_layer(r"sclera|cornea|retina|vitreous|^.*\biris\b|choroid(?! plexus)|eyeball", {1, 3}, exclude=r"plexus") + # \bretina\b (not bare "retina") so it does not match "retinaculum" — the wrist/ankle + # retinacula are connective (layer 7), not the eye's retina. + assert_layer(r"sclera|cornea|\bretina\b|vitreous|^.*\biris\b|choroid(?! plexus)|eyeball", {1, 3}, exclude=r"plexus") # brain → nervous assert_layer(r"\bbrain\b|cerebral cortex|cerebellum", {6}, exclude=r"artery|vein|vessel") # femur → skeleton @@ -183,6 +186,11 @@ def assert_layer(label_pat, want_layers, exclude=None): # aorta / vena cava trunks → vessel (exclude organ-supply *branches* of the aorta, # which correctly carry their target organ's tissue) assert_layer(r"\baorta\b|vena cava", {5}, exclude=r"branch|oesophageal|bronchial") + # connective → connective layer 7, NEVER organ. FMA files ligament/tendon/membrane + # under /viscera/solid_organ/ligament_organ, so without the connective TYPEKEY the + # is_a walk tags them viscus→organ and limb ligaments float in the organ view. + assert_layer(r"interosseous membrane|calcaneal tendon|long plantar ligament|iliotibial tract", + {7}) print(f"\nsummary: QA-1 flagged {len(q1)} · QA-2 flagged {q2} · QA-3 {'FAILED ' + str(fails) if fails else 'passed'}") sys.exit(1 if fails else 0) diff --git a/crates/osint-bake/tools/bake_body_v3.py b/crates/osint-bake/tools/bake_body_v3.py index dd925a1a5..4888c9cda 100644 --- a/crates/osint-bake/tools/bake_body_v3.py +++ b/crates/osint-bake/tools/bake_body_v3.py @@ -50,6 +50,7 @@ "bone": 4, "cartilage": 4, "artery": 5, "vein": 5, "vessel": 5, "nerve": 6, + "connective": 7, # ligaments / tendons / membranes / fascia / aponeuroses / retinacula } # default → 8 "other" diff --git a/crates/osint-bake/tools/bake_torso_splat.py b/crates/osint-bake/tools/bake_torso_splat.py index 8956f86be..4684205ea 100644 --- a/crates/osint-bake/tools/bake_torso_splat.py +++ b/crates/osint-bake/tools/bake_torso_splat.py @@ -64,6 +64,15 @@ ("cartilage", ["cartilage"]), ("bone", ["bone organ", "bone", "skeletal element"]), ("muscle", ["muscle organ", "muscle"]), + # Connective tissue (ligaments / tendons / membranes / fascia / aponeuroses / + # retinacula). FMA files these under /viscera/solid_organ/ligament_organ/ and + # /membrane_organ/, so without this the is_a walk reaches the "viscus" key and + # tags them as ORGANs — limb ligaments (interosseous membrane of leg/forearm, + # calcaneal tendon, long plantar ligament) then floated in the organ view as + # tan limb-shaped strays. Placed AFTER muscle (a real muscle has none of these + # in its type chain) and BEFORE the organ-group keys so it wins over viscus. + ("connective", ["ligament organ", "membrane organ", "tendon", "aponeurosis", + "fascia", "retinaculum"]), ("heart", ["cardiac", "heart", "myocardi"]), ("artery", ["arterial", "artery"]), ("vein", ["venous", "vein"]), @@ -103,6 +112,7 @@ "liver": "alimentary", "gland": "alimentary", "viscus": "viscera", "kidney": "urinary", "nerve": "nervous", "bone": "musculoskeletal", "cartilage": "musculoskeletal", "muscle": "musculoskeletal", + "connective": "musculoskeletal", "skin": "integument", "flesh": "integument", } TISSUE_RGB = { @@ -111,17 +121,17 @@ "heart": (168, 72, 71), "lung": (211, 152, 156), "liver": (139, 82, 76), "kidney": (150, 86, 80), "gi": (201, 167, 131), "gland": (206, 179, 150), "viscus": (188, 132, 120), "vessel": (180, 90, 110), "skin": (214, 178, 162), - "flesh": (199, 160, 150), + "flesh": (199, 160, 150), "connective": (224, 219, 204), } TISSUE_OPACITY = { "skin": 0.14, "flesh": 0.45, "muscle": 0.55, "cartilage": 0.70, "bone": 0.92, "heart": 0.90, "lung": 0.82, "liver": 0.92, "kidney": 0.92, "gi": 0.90, "gland": 0.90, "viscus": 0.90, "artery": 0.96, "vein": 0.96, - "vessel": 0.94, "nerve": 0.97, + "vessel": 0.94, "nerve": 0.97, "connective": 0.60, } TISSUE_CONTAINERS = ["bone", "cartilage", "muscle", "artery", "vein", "vessel", "heart", "lung", "liver", "kidney", "gi", "gland", "viscus", - "nerve", "skin", "flesh"] + "nerve", "skin", "flesh", "connective"] CONTAINER_ID = {t: i for i, t in enumerate(TISSUE_CONTAINERS)} From 150a7732e7da46c705d885b0928d974c660e6e48 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 12:36:44 +0000 Subject: [PATCH 2/3] /genome: endless procedural double-helix viewer (prototype, standalone) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new /genome route — an infinite golden-angle double helix that visualises the GUID address space CPIC lives in, rather than a sized mesh. Standalone (GenomeHelix.tsx), shares nothing with the working /cpic CpicCockpit, so it can never break it (same isolation discipline as /helix vs /body). Cheap by construction — pure repetition placed by a function of the address: - One instanced sugar-bead per strand + one instanced rung, a fixed pool of 240 base-pairs (THREE.InstancedMesh). Position is a pure function of the integer step: angle = step·goldenAngle, y = step·rise. No baked geometry. - Endless: the 240 instances are a sliding WINDOW over an infinite strand — scroll advances, the same instances re-address higher steps, ends fade into fog. Infinite length, constant instance count, no reallocation. - Fractal: the wheel grows density; spacing subdivides ×16 per tier, so zoom descends the 16-ary cascade ("scale = the next cascade level"). The golden angle (most-irrational) makes the scaffold aperiodic — endless, not looped. - CPIC as sparse lit loci: the 16 canonical CPIC level-A pharmacogenes (CYP2D6, CYP2C19, TPMT, DPYD, SLCO1B1…) light up as labelled white rungs in the vast scaffold — the tiny dataset in a huge address space, made visual. Prototype scope: the gene set is the hardcoded canonical CPIC-A list at placeholder golden-scatter addresses. Next step (not done here): light loci from the real graph via POST /api/cpic/reason (the existing gene→diplotype→ phenotype→drug 2-hop), making each rung an expandable real gene. tsc + vite build clean. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01RhpwkHGgia2TuDFvdnuQdE --- cockpit/src/GenomeHelix.tsx | 191 ++++++++++++++++++++++++++++++++++++ cockpit/src/main.tsx | 8 ++ 2 files changed, 199 insertions(+) create mode 100644 cockpit/src/GenomeHelix.tsx diff --git a/cockpit/src/GenomeHelix.tsx b/cockpit/src/GenomeHelix.tsx new file mode 100644 index 000000000..1f0dcb6b9 --- /dev/null +++ b/cockpit/src/GenomeHelix.tsx @@ -0,0 +1,191 @@ +// /genome — endless procedural double helix. EXPERIMENTAL, standalone (shares nothing +// with /cpic's CpicCockpit), so it can never break the working pharmacogenomics cockpit. +// +// The idea (the user's): the OGAR GUID address space is billions of slots +// (HEEL·HIP·TWIG cascade); CPIC fills almost none of it. So this is NOT a sized mesh — +// it is an ENDLESS scaffold that IS the address space, with the sparse real CPIC genes +// lighting up loci in it. Cheap because it is pure REPETITION: one instanced sugar bead +// and one instanced rung, both PLACED BY A FUNCTION of the integer step (golden-angle +// twist + linear rise), drawn only for a window of steps around a scroll offset — so the +// strand is infinite while the instance count is bounded and constant. No baked geometry, +// no forced shape. Zoom descends the 16-ary cascade: each tier subdivides the spacing ×16 +// (self-similar — "scale = the next cascade level"), the literal fractal of the radix tree. +// +// Next step (documented, not done here): light loci from the real CPIC graph via +// POST /api/cpic/reason instead of the hardcoded gene table below. +import { useEffect, useRef, useState } from 'react'; +import * as THREE from 'three'; + +const PAGE_BG = 0x05070d; +const WINDOW = 240; // base-pairs instanced at once (the visible turns); constant +const RISE = 0.34; // vertical gap between successive base-pairs +const RADIUS = 1.0; // helix radius (strand centre to axis) +const TWIST = 2.399963; // radians/step = the GOLDEN ANGLE → the pattern never exactly +// repeats (aperiodic, the "fractal endlessness" — same most-irrational step the φ-spiral uses). +const TAU = Math.PI * 2; + +// The four bases as a deterministic repeating palette (A·T·G·C). Real DNA isn't periodic, +// but the SCAFFOLD is: the base at a step is a pure function of the step index, so the same +// address always paints the same rung — addressability without storage. +const BASE_RGB = [ + [0xff, 0x6b, 0x57], // A — coral + [0xf2, 0xc9, 0x4c], // T — amber + [0x4c, 0xa6, 0xf2], // G — azure + [0x57, 0xd9, 0x8e], // C — mint +]; +const baseAt = (step: number) => ((step * 2654435761) >>> 0) & 3; // cheap hash → 0..3, stable per step + +// Sparse CPIC loci: real pharmacogenes lit up at fixed addresses in the endless scaffold. +// (Placeholder set — the canonical CPIC level-A genes. Wiring /api/cpic/reason replaces this.) +const GENES = ['CYP2D6', 'CYP2C19', 'CYP2C9', 'CYP3A5', 'TPMT', 'DPYD', 'SLCO1B1', 'UGT1A1', + 'NUDT15', 'VKORC1', 'CYP4F2', 'G6PD', 'HLA-B', 'IFNL3', 'CFTR', 'RYR1']; +// place each gene at a stable, spread-out step (golden-ratio scatter over a wide range) +const LOCI = GENES.map((g, i) => ({ gene: g, step: Math.round(((i + 1) * 0.6180339887 % 1) * 4096) })); +const LOCUS_BY_STEP = new Map(LOCI.map((l) => [l.step, l.gene])); + +function labelSprite(text: string): THREE.Sprite { + const c = document.createElement('canvas'); c.width = 256; c.height = 64; + const x = c.getContext('2d')!; + x.fillStyle = 'rgba(8,12,20,0.0)'; x.fillRect(0, 0, 256, 64); + x.font = 'bold 34px ui-monospace, monospace'; x.textAlign = 'center'; x.textBaseline = 'middle'; + x.fillStyle = '#eaf2ff'; x.shadowColor = '#000'; x.shadowBlur = 6; x.fillText(text, 128, 32); + const t = new THREE.CanvasTexture(c); t.anisotropy = 4; + const s = new THREE.Sprite(new THREE.SpriteMaterial({ map: t, transparent: true, depthWrite: false })); + s.scale.set(0.9, 0.225, 1); return s; +} + +function mount(container: HTMLDivElement, scroll: { current: number }, density: { current: number }, + dirty: { current: boolean }): () => void { + let w = container.clientWidth || window.innerWidth, h = container.clientHeight || window.innerHeight; + const scene = new THREE.Scene(); scene.background = new THREE.Color(PAGE_BG); + scene.fog = new THREE.Fog(PAGE_BG, 6, 16); // ends fade into the dark → reads as endless + const camera = new THREE.PerspectiveCamera(50, w / h, 0.01, 100); camera.position.set(0, 0, 6.2); + const renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setSize(w, h); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + container.appendChild(renderer.domElement); + scene.add(new THREE.AmbientLight(0xffffff, 0.55)); + const key = new THREE.DirectionalLight(0xffffff, 0.9); key.position.set(2, 3, 4); scene.add(key); + + // ── instanced geometry: 2 strands of sugar beads + 1 set of base-pair rungs ── + const bead = new THREE.SphereGeometry(0.085, 10, 8); + const beadMat = new THREE.MeshStandardMaterial({ roughness: 0.5, metalness: 0.1 }); + const strandA = new THREE.InstancedMesh(bead, beadMat, WINDOW); + const strandB = new THREE.InstancedMesh(bead, beadMat, WINDOW); + strandA.instanceColor = new THREE.InstancedBufferAttribute(new Float32Array(WINDOW * 3), 3); + strandB.instanceColor = new THREE.InstancedBufferAttribute(new Float32Array(WINDOW * 3), 3); + const rung = new THREE.CylinderGeometry(0.028, 0.028, 1, 6); rung.rotateZ(Math.PI / 2); // lie along X + const rungMat = new THREE.MeshStandardMaterial({ roughness: 0.6 }); + const rungs = new THREE.InstancedMesh(rung, rungMat, WINDOW); + rungs.instanceColor = new THREE.InstancedBufferAttribute(new Float32Array(WINDOW * 3), 3); + scene.add(strandA, strandB, rungs); + + // a small pool of reusable locus labels (only the few visible in the window) + const LABELS = 10; + const labels: { sprite: THREE.Sprite; text: string }[] = []; + for (let i = 0; i < LABELS; i++) { const s = labelSprite(''); s.visible = false; scene.add(s); labels.push({ sprite: s, text: '' }); } + + const m = new THREE.Matrix4(), q = new THREE.Quaternion(), pos = new THREE.Vector3(), scl = new THREE.Vector3(1, 1, 1); + const cA = new THREE.Color(), cB = new THREE.Color(), cR = new THREE.Color(); + + // place the window. step k (0..WINDOW) maps to ABSOLUTE address = base + k/dens, so as + // `scroll` advances the same WINDOW instances slide along an infinite strand (no realloc), + // and as `density` grows the spacing subdivides ×16 per tier (the fractal cascade zoom). + function layout() { + const dens = density.current; // base-pairs per unit step (16^tierFrac) + const base = scroll.current; + const rise = RISE / dens, twist = TWIST; // finer tiers pack tighter (self-similar) + let li = 0; + for (let k = 0; k < WINDOW; k++) { + const step = base + k; // integer address in this tier + const ang = step * twist; + const y = (k - WINDOW / 2) * rise; + const ax = Math.cos(ang) * RADIUS, az = Math.sin(ang) * RADIUS; + // strand A bead + pos.set(ax, y, az); m.compose(pos, q, scl); strandA.setMatrixAt(k, m); + // strand B bead (opposite side) + pos.set(-ax, y, -az); m.compose(pos, q, scl); strandB.setMatrixAt(k, m); + // rung: midpoint, scaled to span 2·RADIUS, rotated to point along the strand pair + const isLoc = LOCUS_BY_STEP.has(((step % 4096) + 4096) % 4096); + const b = baseAt(step), col = BASE_RGB[b]; + cA.setRGB(col[0] / 255, col[1] / 255, col[2] / 255); + strandA.setColorAt(k, cA.clone().multiplyScalar(0.8)); + strandB.setColorAt(k, cB.setRGB(col[2] / 255, col[1] / 255, col[0] / 255).multiplyScalar(0.8)); + rungPlace(k, ax, y, az); + if (isLoc) cR.setRGB(1, 1, 1); else cR.copy(cA).multiplyScalar(0.65); + rungs.setColorAt(k, cR); + // locus label + if (isLoc && li < LABELS) { + const g = LOCUS_BY_STEP.get(((step % 4096) + 4096) % 4096)!; + const L = labels[li++]; if (L.text !== g) { L.sprite.material.map = labelSprite(g).material.map; L.text = g; } + L.sprite.position.set(0, y + 0.16, 0); L.sprite.visible = true; + } + } + for (; li < LABELS; li++) labels[li].sprite.visible = false; + strandA.instanceMatrix.needsUpdate = strandB.instanceMatrix.needsUpdate = rungs.instanceMatrix.needsUpdate = true; + strandA.instanceColor!.needsUpdate = strandB.instanceColor!.needsUpdate = rungs.instanceColor!.needsUpdate = true; + } + const rq = new THREE.Quaternion(), up = new THREE.Vector3(0, 1, 0); + function rungPlace(k: number, ax: number, y: number, az: number) { + pos.set(0, y, 0); + const len = Math.hypot(ax, az) * 2 || 1e-3; + rq.setFromUnitVectors(up, new THREE.Vector3(ax, 0, az).normalize()); // align rung to the A↔B chord + m.compose(pos, rq, new THREE.Vector3(1, len, 1)); rungs.setMatrixAt(k, m); + } + + // controls: drag = orbit, wheel = descend/ascend tiers (fractal zoom), auto-drift = endless travel + let az = 0, el = 0.0, dragging = false, px = 0, py = 0, dist = 6.2; + const onDown = (e: PointerEvent) => { dragging = true; px = e.clientX; py = e.clientY; }; + const onUp = () => { dragging = false; }; + const onMove = (e: PointerEvent) => { if (!dragging) return; az -= (e.clientX - px) * 0.005; el = Math.max(-1.2, Math.min(1.2, el + (e.clientY - py) * 0.005)); px = e.clientX; py = e.clientY; dirty.current = true; }; + const onWheel = (e: WheelEvent) => { e.preventDefault(); density.current = Math.max(1, Math.min(4096, density.current * (1 - Math.sign(e.deltaY) * 0.06))); dirty.current = true; }; + const el2 = renderer.domElement; + el2.addEventListener('pointerdown', onDown); window.addEventListener('pointerup', onUp); + window.addEventListener('pointermove', onMove); el2.addEventListener('wheel', onWheel, { passive: false }); + const onResize = () => { w = container.clientWidth; h = container.clientHeight; camera.aspect = w / h; camera.updateProjectionMatrix(); renderer.setSize(w, h); dirty.current = true; }; + window.addEventListener('resize', onResize); + + let raf = 0, lastScroll = NaN, lastDens = NaN; + const tick = () => { + raf = requestAnimationFrame(tick); + scroll.current += 0.06; // gentle endless travel up the strand + if (scroll.current !== lastScroll || density.current !== lastDens) { layout(); lastScroll = scroll.current; lastDens = density.current; } + camera.position.set(dist * Math.cos(el) * Math.sin(az), dist * Math.sin(el), dist * Math.cos(el) * Math.cos(az)); + camera.lookAt(0, 0, 0); + renderer.render(scene, camera); + dirty.current = false; + }; + tick(); + return () => { + cancelAnimationFrame(raf); + el2.removeEventListener('pointerdown', onDown); window.removeEventListener('pointerup', onUp); + window.removeEventListener('pointermove', onMove); el2.removeEventListener('wheel', onWheel); + window.removeEventListener('resize', onResize); + bead.dispose(); rung.dispose(); beadMat.dispose(); rungMat.dispose(); renderer.dispose(); + if (el2.parentElement === container) container.removeChild(el2); + }; +} + +export default function GenomeHelix() { + const ref = useRef(null); + const scroll = useRef(0); + const density = useRef(1); + const dirty = useRef(true); + const [, force] = useState(0); + useEffect(() => { const c = ref.current; if (!c) return; return mount(c, scroll, density, dirty); }, []); + // light re-render so the tier readout updates as you zoom + useEffect(() => { const id = setInterval(() => force((n) => n + 1), 250); return () => clearInterval(id); }, []); + const tier = Math.log(density.current) / Math.log(16); + return ( +
+
+
+
/genome — endless pharmacogenomic helix
+
+ {WINDOW} instanced base-pairs · golden-angle scaffold · {GENES.length} CPIC loci lit · + tier {tier.toFixed(2)} (16{tier.toFixed(1)} bp/step) +
+
drag = orbit · wheel = descend the 16-ary cascade
+
+
+ ); +} diff --git a/cockpit/src/main.tsx b/cockpit/src/main.tsx index 42db01bca..04cea8775 100644 --- a/cockpit/src/main.tsx +++ b/cockpit/src/main.tsx @@ -15,6 +15,7 @@ import { TorsoMap } from './TorsoMap'; import { FmaBody } from './FmaBody'; import { BodyV3 } from './BodyV3'; import BodyHelix from './BodyHelix'; +import GenomeHelix from './GenomeHelix'; import { CpicCockpit } from './CpicCockpit'; import { ReasoningPage } from './ReasoningPage'; import { ErrorBoundary } from './components/ErrorBoundary'; @@ -116,6 +117,13 @@ createRoot(document.getElementById('root')!).render( via POST /api/cpic/reason (the standalone cpic crate). Additive, gene-first alternative to the organ-first /fma-body. */} } /> + {/* /genome — EXPERIMENTAL endless procedural double helix (GenomeHelix.tsx, + standalone so it can never break /cpic). The GUID address space is billions + of slots; CPIC fills almost none — so this is an infinite golden-angle + scaffold (one instanced base-pair placed by a function of the step, windowed) + with the real pharmacogenes lit as sparse loci. Wheel descends the 16-ary + cascade (self-similar). Next: feed loci from /api/cpic/reason. */} + } /> {/* The Palantir JSON-graph cockpit (221 aiwar nodes) stays reachable at /palantir and as the catch-all for its own sub-routes. */} } /> From 360a38625762ef3d55fd2700e48421a611dfdccb Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 14:51:37 +0000 Subject: [PATCH 3/3] /genome: light loci from the live CPIC catalogue + click-through to /cpic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the hardcoded gene placeholders with the REAL pharmacogene set: - GET /api/cpic/catalog on mount → the live gene list lights the loci; the canonical CPIC-A list stays only as a fallback when the endpoint is absent (old deploy), so /genome always renders. Header shows live vs fallback + the real gene count. - Each gene gets a STABLE address from an FNV-1a hash of its name (linear-probe on the rare collision) → same gene, same locus forever; addressability with zero storage, the whole point of the scaffold. - Click a lit locus (drag-aware: ignored while orbiting) raycasts the rung InstancedMesh and hands off to the working reasoner at /cpic?gene=. - CpicCockpit now honors ?gene= as its initial gene (additive, backward- compatible — no param ⇒ the existing default), so the handoff lands on the clicked gene and its diplotype→phenotype→recommendation 2-hop. The scaffold (golden-angle, windowed-endless, 16-ary zoom) is unchanged; only the loci became real and clickable. tsc + vite build clean. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01RhpwkHGgia2TuDFvdnuQdE --- cockpit/src/CpicCockpit.tsx | 3 +- cockpit/src/GenomeHelix.tsx | 90 +++++++++++++++++++++++++++++-------- 2 files changed, 74 insertions(+), 19 deletions(-) diff --git a/cockpit/src/CpicCockpit.tsx b/cockpit/src/CpicCockpit.tsx index 2182c925f..33376ffdc 100644 --- a/cockpit/src/CpicCockpit.tsx +++ b/cockpit/src/CpicCockpit.tsx @@ -71,7 +71,8 @@ function levelColor(level: string | null): string { export function CpicCockpit() { const [catalog, setCatalog] = useState({ genes: [], drugs: [] }); - const [gene, setGene] = useState('CYP2C19'); + // honor a ?gene= deep-link (e.g. from /genome's locus click); falls back to the default. + const [gene, setGene] = useState(() => new URLSearchParams(window.location.search).get('gene') || 'CYP2C19'); const [input, setInput] = useState('*2/*2'); const [drug, setDrug] = useState('clopidogrel'); const [outcome, setOutcome] = useState(null); diff --git a/cockpit/src/GenomeHelix.tsx b/cockpit/src/GenomeHelix.tsx index 1f0dcb6b9..8eb8dbf4f 100644 --- a/cockpit/src/GenomeHelix.tsx +++ b/cockpit/src/GenomeHelix.tsx @@ -36,12 +36,25 @@ const BASE_RGB = [ const baseAt = (step: number) => ((step * 2654435761) >>> 0) & 3; // cheap hash → 0..3, stable per step // Sparse CPIC loci: real pharmacogenes lit up at fixed addresses in the endless scaffold. -// (Placeholder set — the canonical CPIC level-A genes. Wiring /api/cpic/reason replaces this.) -const GENES = ['CYP2D6', 'CYP2C19', 'CYP2C9', 'CYP3A5', 'TPMT', 'DPYD', 'SLCO1B1', 'UGT1A1', - 'NUDT15', 'VKORC1', 'CYP4F2', 'G6PD', 'HLA-B', 'IFNL3', 'CFTR', 'RYR1']; -// place each gene at a stable, spread-out step (golden-ratio scatter over a wide range) -const LOCI = GENES.map((g, i) => ({ gene: g, step: Math.round(((i + 1) * 0.6180339887 % 1) * 4096) })); -const LOCUS_BY_STEP = new Map(LOCI.map((l) => [l.step, l.gene])); +// The gene list is pulled LIVE from GET /api/cpic/catalog; this canonical CPIC level-A set +// is only the fallback when the endpoint is absent (old deploy) so /genome still renders. +const FALLBACK_GENES = ['CYP2D6', 'CYP2C19', 'CYP2C9', 'CYP3A5', 'TPMT', 'DPYD', 'SLCO1B1', + 'UGT1A1', 'NUDT15', 'VKORC1', 'CYP4F2', 'G6PD', 'HLA-B', 'IFNL3', 'CFTR', 'RYR1']; +type Locus = { step: number; gene: string }; +// Each gene gets a STABLE address from a hash of its name (FNV-1a) → a step in [0,4096). +// Same gene ⇒ same locus forever (addressability without storage), spread across the tier. +function lociFrom(genes: string[]): Locus[] { + const seen = new Map(); + const out: Locus[] = []; + for (const g of genes) { + let hsh = 2166136261; + for (let i = 0; i < g.length; i++) { hsh ^= g.charCodeAt(i); hsh = Math.imul(hsh, 16777619); } + let step = (hsh >>> 0) % 4096; + while (seen.has(step)) step = (step + 1) % 4096; // linear-probe the rare collision + seen.set(step, g); out.push({ step, gene: g }); + } + return out; +} function labelSprite(text: string): THREE.Sprite { const c = document.createElement('canvas'); c.width = 256; c.height = 64; @@ -55,7 +68,7 @@ function labelSprite(text: string): THREE.Sprite { } function mount(container: HTMLDivElement, scroll: { current: number }, density: { current: number }, - dirty: { current: boolean }): () => void { + dirty: { current: boolean }, locusByStep: Map): () => void { let w = container.clientWidth || window.innerWidth, h = container.clientHeight || window.innerHeight; const scene = new THREE.Scene(); scene.background = new THREE.Color(PAGE_BG); scene.fog = new THREE.Fog(PAGE_BG, 6, 16); // ends fade into the dark → reads as endless @@ -78,6 +91,7 @@ function mount(container: HTMLDivElement, scroll: { current: number }, density: const rungs = new THREE.InstancedMesh(rung, rungMat, WINDOW); rungs.instanceColor = new THREE.InstancedBufferAttribute(new Float32Array(WINDOW * 3), 3); scene.add(strandA, strandB, rungs); + const geneOf: (string | null)[] = new Array(WINDOW).fill(null); // instance k → gene (for picking) // a small pool of reusable locus labels (only the few visible in the window) const LABELS = 10; @@ -105,7 +119,9 @@ function mount(container: HTMLDivElement, scroll: { current: number }, density: // strand B bead (opposite side) pos.set(-ax, y, -az); m.compose(pos, q, scl); strandB.setMatrixAt(k, m); // rung: midpoint, scaled to span 2·RADIUS, rotated to point along the strand pair - const isLoc = LOCUS_BY_STEP.has(((step % 4096) + 4096) % 4096); + const addr = ((step % 4096) + 4096) % 4096; + const isLoc = locusByStep.has(addr); + geneOf[k] = isLoc ? locusByStep.get(addr)! : null; const b = baseAt(step), col = BASE_RGB[b]; cA.setRGB(col[0] / 255, col[1] / 255, col[2] / 255); strandA.setColorAt(k, cA.clone().multiplyScalar(0.8)); @@ -115,7 +131,7 @@ function mount(container: HTMLDivElement, scroll: { current: number }, density: rungs.setColorAt(k, cR); // locus label if (isLoc && li < LABELS) { - const g = LOCUS_BY_STEP.get(((step % 4096) + 4096) % 4096)!; + const g = geneOf[k]!; const L = labels[li++]; if (L.text !== g) { L.sprite.material.map = labelSprite(g).material.map; L.text = g; } L.sprite.position.set(0, y + 0.16, 0); L.sprite.visible = true; } @@ -133,10 +149,26 @@ function mount(container: HTMLDivElement, scroll: { current: number }, density: } // controls: drag = orbit, wheel = descend/ascend tiers (fractal zoom), auto-drift = endless travel - let az = 0, el = 0.0, dragging = false, px = 0, py = 0, dist = 6.2; - const onDown = (e: PointerEvent) => { dragging = true; px = e.clientX; py = e.clientY; }; - const onUp = () => { dragging = false; }; - const onMove = (e: PointerEvent) => { if (!dragging) return; az -= (e.clientX - px) * 0.005; el = Math.max(-1.2, Math.min(1.2, el + (e.clientY - py) * 0.005)); px = e.clientX; py = e.clientY; dirty.current = true; }; + // click (no drag) on a lit locus = hand off to the working /cpic reasoner for that gene. + let az = 0, el = 0.0, dragging = false, moved = 0, px = 0, py = 0, dist = 6.2; + const ray = new THREE.Raycaster(); const ndc = new THREE.Vector2(); + const pick = (e: PointerEvent): string | null => { + const r = el2.getBoundingClientRect(); + ndc.set(((e.clientX - r.left) / r.width) * 2 - 1, -((e.clientY - r.top) / r.height) * 2 + 1); + ray.setFromCamera(ndc, camera); + const hit = ray.intersectObject(rungs)[0]; + return hit && hit.instanceId != null ? geneOf[hit.instanceId] : null; + }; + const onDown = (e: PointerEvent) => { dragging = true; moved = 0; px = e.clientX; py = e.clientY; }; + const onUp = (e: PointerEvent) => { + dragging = false; + if (moved < 5) { const g = pick(e); if (g) window.location.assign(`/cpic?gene=${encodeURIComponent(g)}`); } + }; + const onMove = (e: PointerEvent) => { + if (!dragging) return; + moved += Math.abs(e.clientX - px) + Math.abs(e.clientY - py); + az -= (e.clientX - px) * 0.005; el = Math.max(-1.2, Math.min(1.2, el + (e.clientY - py) * 0.005)); px = e.clientX; py = e.clientY; dirty.current = true; + }; const onWheel = (e: WheelEvent) => { e.preventDefault(); density.current = Math.max(1, Math.min(4096, density.current * (1 - Math.sign(e.deltaY) * 0.06))); dirty.current = true; }; const el2 = renderer.domElement; el2.addEventListener('pointerdown', onDown); window.addEventListener('pointerup', onUp); @@ -170,8 +202,30 @@ export default function GenomeHelix() { const scroll = useRef(0); const density = useRef(1); const dirty = useRef(true); + const [genes, setGenes] = useState(null); // null = still loading the catalog + const [live, setLive] = useState(false); // true = real /api/cpic/catalog const [, force] = useState(0); - useEffect(() => { const c = ref.current; if (!c) return; return mount(c, scroll, density, dirty); }, []); + + // pull the REAL CPIC gene catalogue; fall back to the canonical list if the endpoint is + // absent (old deploy) so /genome always renders. Same graceful-degradation as /helix LOD. + useEffect(() => { + let cancelled = false; + fetch('/api/cpic/catalog') + .then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))) + .then((j: { genes?: string[] }) => { + if (cancelled) return; + const gs = (j.genes ?? []).filter(Boolean); + if (gs.length) { setGenes(gs); setLive(true); } else { setGenes(FALLBACK_GENES); } + }) + .catch(() => { if (!cancelled) setGenes(FALLBACK_GENES); }); + return () => { cancelled = true; }; + }, []); + + useEffect(() => { + const c = ref.current; if (!c || !genes) return; + const locusByStep = new Map(lociFrom(genes).map((l) => [l.step, l.gene])); + return mount(c, scroll, density, dirty, locusByStep); + }, [genes]); // light re-render so the tier readout updates as you zoom useEffect(() => { const id = setInterval(() => force((n) => n + 1), 250); return () => clearInterval(id); }, []); const tier = Math.log(density.current) / Math.log(16); @@ -180,11 +234,11 @@ export default function GenomeHelix() {
/genome — endless pharmacogenomic helix
-
- {WINDOW} instanced base-pairs · golden-angle scaffold · {GENES.length} CPIC loci lit · - tier {tier.toFixed(2)} (16{tier.toFixed(1)} bp/step) +
+ {genes ? `${WINDOW} instanced base-pairs · golden-angle scaffold · ${genes.length} CPIC gene loci ${live ? 'lit (live /api/cpic)' : 'lit (fallback list)'} · tier ${tier.toFixed(2)}` + : 'loading CPIC gene catalogue…'}
-
drag = orbit · wheel = descend the 16-ary cascade
+
drag = orbit · wheel = descend the 16-ary cascade · click a lit gene → /cpic
);