feat(manifold): show live model architecture in docks
This commit is contained in:
parent
491820ac79
commit
4db0f498eb
4 changed files with 137 additions and 7 deletions
5
MAP.md
5
MAP.md
|
|
@ -40,7 +40,7 @@ anchor + locked decisions) and the `docs/specs/*-spec.md` set.
|
|||
`worklet/nisps-processor.ts` (audio), thin WASM wrappers over the core pipelines + curve catalog (the TS
|
||||
`input-pipeline`/`output-pipeline`/`curves` implementations died at P4), `wasm-worker.ts`,
|
||||
`spine.ts` (the reactive spine BELOW React — `setInput` derives processed→ml→routed eagerly off-render),
|
||||
`engine-api.ts` (`EngineApi` façade incl. `feedback.*` wrappers over the `nisps_ml_feedback_*` C ABI),
|
||||
`engine-api.ts` (`EngineApi` façade incl. live architecture/weight/example metrics and `feedback.*` wrappers over the `nisps_ml_feedback_*` C ABI),
|
||||
`EngineProvider.tsx`/`useEngine.ts` (React binding via `useSyncExternalStore` version counter). nisps.js is
|
||||
loaded via fetch+indirect-eval (Emscripten MODULARIZE glue has no ES exports), base-aware via `document.baseURI`
|
||||
for the `/next` sub-path.
|
||||
|
|
@ -48,7 +48,8 @@ anchor + locked decisions) and the `docs/specs/*-spec.md` set.
|
|||
- `manifold/src/console/` — the convertible Console: `ConsoleApp`, `CompositeStage` (single-divider convertible
|
||||
with snap/magnetism/minimap-demotion), `OutputStage`/`SandwichStage`/`ParticleStage`/`Manifold` (canvas,
|
||||
rect↔circular + feedback markers), `Dock` (top Mode selector + 5 vertically-centred drawers), `Drawers`
|
||||
(Learning/Inputs/Outputs/Settings/Help), `TrainingHealth` (real per-iteration loss curve from
|
||||
(Learning/Inputs/Outputs/Settings/Help; Learning includes the live model-architecture inspector and
|
||||
expanded Outputs owns the remaining scroll height), `TrainingHealth` (real per-iteration loss curve from
|
||||
`nisps_ml_loss_history` + per-layer weight health from `nisps_ml_get_layer_stats`; rendered only at
|
||||
the Learning drawer's `expanded` depth — that IS the advanced-surface flag), `VerdictCluster`
|
||||
(mode-aware), `OutputEditor`/`DualRange`/`CurvePad`, `icons.tsx`
|
||||
|
|
|
|||
|
|
@ -146,7 +146,8 @@ for the narrow pane.
|
|||
icons, vertically centred macOS-dock style; **BOTTOM** = sandwich toggle.
|
||||
- Five drawers (`DRAWERS` in `Drawers.tsx`, each has `.render(ctx, depth)` — condensed 360px panel
|
||||
vs expanded 80vw×80vh modal):
|
||||
- **learn** — feedback mode (explore-and-place / geometric-dislike) + solo mode + per-output arm.
|
||||
- **learn** — feedback mode (explore-and-place / geometric-dislike) + solo mode + per-output arm,
|
||||
plus a live model-architecture strip (inputs/outputs highlighted; hidden layers expand on click).
|
||||
At `expanded` depth ONLY it also renders `TrainingHealth.tsx`: the real per-iteration loss
|
||||
curve (`EngineApi.lossHistory()` ← `nisps_ml_loss_history` ← `MLPCore::loss_history`) plus
|
||||
the per-layer weight-health table (`getLayerStats`). **`depth === 'expanded'` is Manifold's
|
||||
|
|
@ -154,7 +155,8 @@ for the narrow pane.
|
|||
surface there rather than inventing one. The panel renders "no training run yet" when the
|
||||
core has no history; it never synthesises a curve.
|
||||
- **inputs** — enable/configure input sources (XY pad / MIDI / gamepad).
|
||||
- **route** (label "Outputs") — per-output control matrix + per-backend config.
|
||||
- **route** (label "Outputs") — per-output control matrix + per-backend config; at expanded depth
|
||||
the output rows fill the remaining drawer height and scroll independently.
|
||||
- **settings** — icon style (monochrome/colour), input-map shape (xy/joystick/rect/circular), corner radius.
|
||||
- **help** — keymap pills + loop explanation.
|
||||
- `src/dock/` holds the output-routing internals used by the `route` drawer:
|
||||
|
|
@ -200,7 +202,7 @@ a setting → `--r-*` tokens.
|
|||
- `engine-api.ts` — **`EngineApi`, the framework-neutral facade** everything in the UI talks to:
|
||||
`setInput/setInputs`, `getOutputs/routedOutput`, training (`addExample/train/trainAsync/evalLoss`),
|
||||
weights (`getWeights/setWeights/process/randomise`), telemetry
|
||||
(`lossHistory/getLayerStats`), `subscribe/version/on`, plus nested `.feedback` and `.audio`
|
||||
(`architecture/weightCount/exampleCount/lossHistory/getLayerStats`), `subscribe/version/on`, plus nested `.feedback` and `.audio`
|
||||
facades. **`lossHistory()` reads SPINE STATE, not the MLP handle** — an async train runs on the
|
||||
worker's mirror net, so the main handle's own history is empty for those runs; both paths
|
||||
publish to the spine.
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
* Where engine support does not yet exist the UI + state are wired and a TODO
|
||||
* references the relevant spec — no faked engine behaviour.
|
||||
*/
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import { Badge, Button, PillToggle, Slider, Switch } from '../primitives';
|
||||
import type { ConsoleCtx, DrawerDepth, DrawerKey, FeedbackModeUI, SoloMode } from './types';
|
||||
import type { InputMode } from '../inputs';
|
||||
|
|
@ -31,6 +31,7 @@ import { outputModeDescriptor } from './output-mode';
|
|||
import { useSettings, unfocusedIconCss } from '../settings/settings-store';
|
||||
import type { UnfocusedIconColour, InputMapMode } from '../settings/settings-store';
|
||||
import type { ExampleResizePolicy, NetworkResizePolicy } from '../engine/io-reshape';
|
||||
import { useEngine, useEngineVersion } from '../engine';
|
||||
import { EditorPanel } from '../serial/EditorPanel';
|
||||
import { TrainingHealth } from './TrainingHealth';
|
||||
import {
|
||||
|
|
@ -157,6 +158,119 @@ const SOLO_DESC: Record<SoloMode, string> = {
|
|||
'dont-care': 'Each example stores a per-output mask so stale labels never pull unarmed outputs.',
|
||||
};
|
||||
|
||||
function ModelArchitecture() {
|
||||
const engine = useEngine();
|
||||
useEngineVersion(engine);
|
||||
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||
|
||||
if (!engine) {
|
||||
return <p style={{ fontSize: 10, color: 'var(--fg-dim)', margin: 0 }}>engine not ready</p>;
|
||||
}
|
||||
|
||||
const arch = engine.architecture;
|
||||
const hidden = arch.hidden.filter((size) => size > 0);
|
||||
const layerCount = arch.numLayers || hidden.length + 1;
|
||||
const sizes = hidden.map((size) => size.toString()).join(' → ');
|
||||
const stageStyle = (tone: string) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
gap: 2,
|
||||
minWidth: 0,
|
||||
padding: '6px 8px',
|
||||
border: `1px solid ${tone}`,
|
||||
borderRadius: 'var(--r-1)',
|
||||
background: 'var(--bg-2)',
|
||||
});
|
||||
const labelStyle = {
|
||||
fontSize: 9,
|
||||
fontFamily: 'var(--font-mono)',
|
||||
letterSpacing: '0.08em',
|
||||
color: 'var(--fg-dim)',
|
||||
};
|
||||
const valueStyle = {
|
||||
fontSize: 12,
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
color: 'var(--fg)',
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-testid="model-architecture" style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||
<Chip tone="var(--accent)">
|
||||
{layerCount} layer{layerCount === 1 ? '' : 's'}
|
||||
</Chip>
|
||||
<Chip>{engine.weightCount.toLocaleString()} weights</Chip>
|
||||
<Chip>{engine.exampleCount} examples</Chip>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto 1fr', gap: 4, alignItems: 'stretch' }}>
|
||||
<div style={stageStyle('var(--accent-2)')}>
|
||||
<span style={{ ...labelStyle, color: 'var(--accent-2)' }}>INPUT</span>
|
||||
<span style={valueStyle}>{arch.inputSize} units</span>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{ alignSelf: 'center', color: 'var(--fg-dim)', fontFamily: 'var(--font-mono)', fontSize: 11 }}
|
||||
>
|
||||
→
|
||||
</div>
|
||||
<div style={stageStyle('var(--accent)')}>
|
||||
<span style={{ ...labelStyle, color: 'var(--accent)' }}>OUTPUT</span>
|
||||
<span style={valueStyle}>{arch.outputSize} units</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={detailsOpen}
|
||||
onClick={() => setDetailsOpen((open) => !open)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
width: '100%',
|
||||
padding: '5px 8px',
|
||||
border: '1px solid var(--line)',
|
||||
borderRadius: 'var(--r-1)',
|
||||
background: 'var(--bg-2)',
|
||||
color: 'var(--fg-mute)',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 10,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--accent)' }}>{detailsOpen ? '▾' : '▸'}</span>
|
||||
<span style={{ color: 'var(--fg)' }}>HIDDEN</span>
|
||||
<span>{hidden.length} layer{hidden.length === 1 ? '' : 's'}</span>
|
||||
{!detailsOpen && <span style={{ marginLeft: 'auto' }}>{sizes || 'none'}</span>}
|
||||
</button>
|
||||
{detailsOpen && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 3, paddingLeft: 12 }}>
|
||||
{hidden.length > 0 ? (
|
||||
hidden.map((size, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
fontSize: 10,
|
||||
fontFamily: 'var(--font-mono)',
|
||||
color: 'var(--fg-mute)',
|
||||
}}
|
||||
>
|
||||
<span>HIDDEN {i + 1}</span>
|
||||
<span style={{ color: 'var(--fg)' }}>{size} units</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<span style={{ fontSize: 10, color: 'var(--fg-dim)' }}>no hidden layers</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LearningDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
||||
return (
|
||||
<>
|
||||
|
|
@ -227,6 +341,9 @@ function LearningDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<SectionLabel>Current model</SectionLabel>
|
||||
<ModelArchitecture />
|
||||
|
||||
{depth === 'expanded' && (
|
||||
<>
|
||||
<SectionLabel>Solo / arm scope</SectionLabel>
|
||||
|
|
@ -646,7 +763,9 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
|||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
maxHeight: expanded ? 460 : 220,
|
||||
flex: expanded ? 1 : undefined,
|
||||
minHeight: expanded ? 0 : undefined,
|
||||
maxHeight: expanded ? undefined : 220,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -404,6 +404,14 @@ export class EngineApi {
|
|||
return this.iml.architecture;
|
||||
}
|
||||
|
||||
get weightCount(): number {
|
||||
return this.iml.weightCount;
|
||||
}
|
||||
|
||||
get exampleCount(): number {
|
||||
return this.iml.exampleCount;
|
||||
}
|
||||
|
||||
// ---- Direct handle access (advanced consumers; spine pipelines, etc.) ----
|
||||
get ml(): WasmIML {
|
||||
return this.iml;
|
||||
|
|
|
|||
Loading…
Reference in a new issue