It's a sine

The smallest end-to-end tensorgrad training loop: a 3-layer MLP learns y = sin(x).

---
format: typebulb/v1
name: "It's a sine"
---

**code.tsx**

```tsx
import { Module, compile, Linear, mul, sub, mean, relu, type Tensor } from 'tensorgrad'

const HIDDEN = 64
const BATCH_SIZE = 256
const LR = 0.005

class MLP extends Module {
  l1 = new Linear(1, HIDDEN)
  l2 = new Linear(HIDDEN, HIDDEN)
  l3 = new Linear(HIDDEN, 1)
}

function modelFwd(p: MLP, x: Tensor): Tensor {
  return p.l3.fwd(relu(p.l2.fwd(relu(p.l1.fwd(x)))))
}

function lossFn(p: MLP, { x, y }: { x: Tensor; y: Tensor }): Tensor {
  const diff = sub(modelFwd(p, x), y)
  return mean(mul(diff, diff))
}

function makeBatch(): { x: Float32Array; y: Float32Array } {
  const x = new Float32Array(BATCH_SIZE)
  const y = new Float32Array(BATCH_SIZE)
  for (let i = 0; i < BATCH_SIZE; i++) {
    const v = (Math.random() * 2 - 1) * Math.PI
    x[i] = v
    y[i] = Math.sin(v)
  }
  return { x, y }
}

// Build the training graph + a polymorphic-batch inference graph that shares
// its params. Returns both plus the timestamped compile duration so the UI
// can show how long it took.
async function buildGraphs() {
  const t0 = performance.now()
  const model = new MLP()
  const train = await compile({
    model,
    loss: lossFn,
    optimizer: { kind: 'adam', lr: LR },
    inputs: { x: [BATCH_SIZE, 1], y: [BATCH_SIZE, 1] },
  })
  const infer = await train.attach({
    forward: (m, { x }) => modelFwd(m, x),
    inputs: { x: [null, 1] },
  })
  return { train, infer, compileMs: performance.now() - t0 }
}

const PLOT_N = 200
const canvas = document.getElementById('plot') as HTMLCanvasElement
const ctx = canvas.getContext('2d')!
const statusEl = document.getElementById('status') as HTMLDivElement
const runBtn = document.getElementById('run') as HTMLButtonElement
const stopBtn = document.getElementById('stop') as HTMLButtonElement

const plotXs = new Float32Array(PLOT_N)
for (let i = 0; i < PLOT_N; i++) plotXs[i] = -Math.PI + (2 * Math.PI) * i / (PLOT_N - 1)

function drawPlot(modelYs: Float32Array): void {
  const w = canvas.width, h = canvas.height
  const style = getComputedStyle(document.documentElement)
  ctx.clearRect(0, 0, w, h)
  drawCurve(style.getPropertyValue('--axis').trim(),   1, (_, i) => h / 2)
  drawCurve(style.getPropertyValue('--target').trim(), 1, (x) => h / 2 - Math.sin(x) * (h * 0.4))
  drawCurve(style.getPropertyValue('--model').trim(),  2, (_, i) => h / 2 - modelYs[i]! * (h * 0.4))

  function drawCurve(color: string, width: number, y: (x: number, i: number) => number): void {
    ctx.strokeStyle = color
    ctx.lineWidth = width
    ctx.beginPath()
    for (let i = 0; i < PLOT_N; i++) {
      const px = (plotXs[i]! + Math.PI) / (2 * Math.PI) * w
      if (i === 0) ctx.moveTo(px, y(plotXs[i]!, i)); else ctx.lineTo(px, y(plotXs[i]!, i))
    }
    ctx.stroke()
  }
}

drawPlot(new Float32Array(PLOT_N))   // initial paint so the target curve is visible before training

let stopRequested = false
stopBtn.onclick = () => { stopRequested = true }

runBtn.onclick = async () => {
  runBtn.disabled = true
  stopBtn.disabled = false
  stopRequested = false
  statusEl.textContent = 'Compiling…'

  const { train, infer, compileMs } = await buildGraphs()
  statusEl.textContent = `Compiled in ${compileMs.toFixed(0)} ms. Training…`

  let step = 0, lastViz = 0
  try {
    while (!stopRequested) {
      step++
      const r = await train.step(makeBatch())
      if (r.kind !== 'completed') break
      const now = performance.now()
      if (now - lastViz > 200) {
        lastViz = now
        const pr = await infer.run({ x: plotXs })
        if (pr.kind === 'completed') drawPlot(pr.output)
        statusEl.textContent = `step ${step}   loss ${r.loss.toFixed(6)}`
      }
      if (step % 5 === 0) await new Promise(r => setTimeout(r, 0))
    }
    statusEl.textContent = `Stopped at step ${step}.`
  } catch (e) {
    statusEl.textContent = `error: ${(e as { message?: string })?.message ?? e}`
  } finally {
    infer.destroy()
    train.destroy()
    runBtn.disabled = false
    stopBtn.disabled = true
  }
}
```
**styles.css**

```css
:root {
  color-scheme: light;
  --bg: #ffffff;
  --fg: #1a1a1a;
  --muted: #666666;
  --border: #dddddd;
  --axis: #eeeeee;
  --target: #9a9a9a;
  --model: #0d9488;
  --accent: #0d9488;
  --accent-fg: #ffffff;
  --btn-bg: #ffffff;
  --btn-border: #bbbbbb;
}

html[data-theme="dark"] {
  color-scheme: dark;
  --bg: #1c1c1c;
  --fg: #ececec;
  --muted: #a0a0a0;
  --border: #333333;
  --axis: #2a2a2a;
  --target: #707070;
  --model: #5eead4;
  --accent: #14b8a6;
  --accent-fg: #0f1f1d;
  --btn-bg: #2a2a2a;
  --btn-border: #444444;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

.page {
  max-width: 900px;
  margin: 2rem auto;
  padding: 0 1rem;
}

h1 {
  font-size: 1.4rem;
  margin-bottom: 0.25rem;
  color: var(--accent);
}

.subtitle {
  color: var(--muted);
  margin-top: 0;
  margin-bottom: 1.5rem;
}

.controls {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

button {
  padding: 0.5rem 1rem;
  font-size: 0.95rem;
  cursor: pointer;
  background: var(--btn-bg);
  color: var(--fg);
  border: 1px solid var(--btn-border);
  border-radius: 4px;
}

button.primary {
  background: var(--accent);
  color: var(--accent-fg);
  border-color: var(--accent);
}

button:disabled {
  opacity: 0.5;
  cursor: default;
}

canvas {
  display: block;
  border: 1px solid var(--border);
  border-radius: 6px;
  max-width: 100%;
  height: auto;
}

#status {
  font-family: ui-monospace, monospace;
  font-size: 0.9rem;
  color: var(--muted);
  margin-top: 0.75rem;
  min-height: 1.4em;
}

code {
  font-family: ui-monospace, monospace;
  font-size: 0.9em;
  background: var(--border);
  padding: 0.05rem 0.3rem;
  border-radius: 3px;
}
```
**index.html**

```html
<div class="page">
  <h1>It's a sine</h1>
  <p class="subtitle">
    A 3-layer MLP (1 → 64 → 64 → 1) learns to approximate <code>y = sin(x)</code>
    over [-π, π]. The faint curve is the target; the bold curve is the
    model's prediction, updated every ~200 ms during training.
  </p>
  <div class="controls">
    <button id="run" class="primary">Train</button>
    <button id="stop" disabled>Stop</button>
  </div>
  <canvas id="plot" width="800" height="280"></canvas>
  <div id="status">ready.</div>
</div>
```
**config.json**

```json
{
  "dependencies": {
    "tensorgrad": "^0.4.2"
  },
  "description": "The smallest end-to-end tensorgrad training loop: a 3-layer MLP learns y = sin(x)."
}
```

Markdown source · More bulbs by samples · Typebulb home