Ryan Malloy 00fa420743 Add iframe auto-resize via postMessage
Embeds now report content height to the parent frame using a
ResizeObserver on document.body. The EmbedDialog snippet includes
a listener script that adjusts the iframe height on each resize
event. Documented the spicebook-resize protocol in llms.txt.
2026-03-07 23:45:10 -07:00

624 lines
14 KiB
Plaintext

# SpiceBook
> Notebook interface for SPICE circuit simulation. Create, edit, and run SPICE netlists in a cell-based notebook UI with waveform visualization and schematic generation. Supports ngspice and LTspice engines.
Base URL: `https://spicebook.warehack.ing`
## API Reference
All endpoints accept and return JSON unless noted otherwise. Prefix all paths with the base URL.
---
### Notebooks
#### List notebooks
```
GET /api/notebooks
```
**Response** `200`
```json
[
{
"id": "rc-low-pass-a1b2c3d4",
"title": "RC Low-Pass Filter",
"engine": "ngspice",
"tags": ["filter", "rc"],
"cell_count": 3,
"modified": "2026-02-13T18:30:00+00:00"
}
]
```
#### Create notebook
```
POST /api/notebooks
```
**Request body**
```json
{
"title": "My Circuit",
"engine": "ngspice"
}
```
Both fields are optional. Defaults: title = `"Untitled Notebook"`, engine = `"ngspice"`. Supported engines: `"ngspice"`, `"ltspice"`.
**Response** `201`
```json
{
"id": "my-circuit-f8e2a91b",
"spicebook_version": "2026-02-13",
"metadata": {
"title": "My Circuit",
"engine": "ngspice",
"tags": [],
"created": "2026-02-13T18:30:00+00:00",
"modified": "2026-02-13T18:30:00+00:00"
},
"cells": [
{
"id": "cell-a1b2c3d4e5f6",
"type": "markdown",
"source": "# My Circuit\n\nAdd SPICE cells below to begin simulating.",
"outputs": []
}
]
}
```
#### Get notebook
```
GET /api/notebooks/{notebook_id}
```
**Response** `200` — Full `Notebook` object (same shape as create response, without `id` wrapper).
#### Update notebook
```
PUT /api/notebooks/{notebook_id}
```
**Request body** — Full `Notebook` object (replaces entire notebook).
**Response** `200` — The saved `Notebook`.
#### Delete notebook
```
DELETE /api/notebooks/{notebook_id}
```
**Response** `204` — No content. Only user-created notebooks can be deleted.
---
### Cells
Cells are ordered elements within a notebook. Each cell has a `type` (`markdown`, `spice`, `python`, `schematic`) and `source` (text content).
#### Add cell
```
POST /api/notebooks/{notebook_id}/cells
```
**Request body**
```json
{
"type": "spice",
"source": "V1 1 0 DC 5\nR1 1 0 1k\n.op\n.end",
"after_cell_id": "cell-a1b2c3d4e5f6"
}
```
`after_cell_id` is optional — omit to append at end.
**Response** `201`
```json
{
"id": "cell-b2c3d4e5f6a7",
"type": "spice",
"source": "V1 1 0 DC 5\nR1 1 0 1k\n.op\n.end",
"outputs": []
}
```
#### Update cell
```
PUT /api/notebooks/{notebook_id}/cells/{cell_id}
```
**Request body**
```json
{
"source": "V1 1 0 DC 10\nR1 1 0 2k\n.op\n.end",
"type": "spice"
}
```
Both fields optional — only provided fields are updated.
**Response** `200` — Updated `Cell`.
#### Delete cell
```
DELETE /api/notebooks/{notebook_id}/cells/{cell_id}
```
**Response** `204`
#### Reorder cells
```
PUT /api/notebooks/{notebook_id}/cells/reorder
```
**Request body**
```json
{
"cell_ids": ["cell-b2c3d4e5f6a7", "cell-a1b2c3d4e5f6"]
}
```
Must include every cell ID exactly once.
**Response** `200` — Array of `Cell` objects in new order.
---
### Simulation
#### Run standalone simulation
```
POST /api/simulate
```
**Request body**
```json
{
"netlist": "V1 1 0 DC 5\nR1 1 2 1k\nR2 2 0 2k\n.op\n.end",
"engine": "ngspice"
}
```
**Response** `200`
```json
{
"success": true,
"waveform": {
"variables": [
{"name": "v(1)", "type": "voltage"},
{"name": "v(2)", "type": "voltage"}
],
"points": 1,
"x_data": [0.0],
"y_data": {"v(1)": [5.0], "v(2)": [3.333]},
"x_type": "time",
"is_complex": false,
"y_magnitude_db": null,
"y_phase_deg": null
},
"log": "ngspice output...",
"error": null,
"elapsed_seconds": 0.42
}
```
#### Run cell in notebook
```
POST /api/notebooks/{notebook_id}/cells/{cell_id}/run
```
No request body — uses the cell's `source` as the netlist and the notebook's `engine`.
**Response** `200` — Same `SimulationResponse` shape. The cell's outputs are updated in the saved notebook.
---
### Schematics
#### Generate schematic from cell
```
POST /api/notebooks/{notebook_id}/cells/{cell_id}/schematic
```
No request body. Cell must be type `spice`.
**Response** `200`
```json
{
"svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" ...>...</svg>",
"success": true,
"error": null,
"component_map": {"R1": "resistor", "V1": "voltage_source"}
}
```
**Color-coded resistors**: Resistors with parseable values between 0.01 and 1e9 ohms render as IEEE zigzag symbols with segments colored according to the standard 4-band resistor color code. A 10k resistor shows Brown-Black-Orange-Gold bands directly on the zigzag. The entry/exit half-segments and the gap before the tolerance band use the default wire color, mimicking the physical spacing that indicates reading direction. Parametric values (e.g. `{R_val}`) and out-of-range resistances fall back to a standard monochrome zigzag.
Band-to-segment mapping (7 zigzag sub-segments):
- Seg 0 (entry): wire color
- Seg 1: 1st significant digit
- Seg 2: 2nd significant digit
- Seg 3: multiplier
- Seg 4: gap (wire color, mimics physical spacing)
- Seg 5: tolerance
- Seg 6 (exit): wire color
---
### Waveforms
#### Generate SVG plot (JSON-wrapped)
```
POST /api/waveforms/svg
```
**Request body**
```json
{
"waveform": { "...WaveformData from simulation response..." },
"title": "Output Voltage",
"width": 800,
"height": 500,
"signals": ["v(2)"]
}
```
`signals` is optional — omit to plot all signals. `width`, `height`, `title` have defaults.
**Response** `200`
```json
{
"svg": "<svg ...>...</svg>"
}
```
#### Generate SVG plot (raw)
```
POST /api/waveforms/svg/raw
```
Same request body as above.
**Response** `200` with `Content-Type: image/svg+xml` — raw SVG string.
---
### Compose (convenience)
Create a fully-populated notebook with multiple cells in a single call.
#### Compose notebook
```
POST /api/notebooks/compose
```
**Request body**
```json
{
"title": "RC Low-Pass Filter",
"engine": "ngspice",
"tags": ["filter", "rc", "analog"],
"cells": [
{
"type": "markdown",
"source": "# RC Low-Pass Filter\n\nA simple first-order low-pass filter."
},
{
"type": "spice",
"source": "V1 in 0 AC 1\nR1 in out 1k\nC1 out 0 1u\n.ac dec 100 1 1meg\n.end"
},
{
"type": "markdown",
"source": "## Analysis\n\nThe -3dB cutoff is at f = 1/(2*pi*R*C) = 159 Hz."
}
],
"run": false
}
```
| Field | Type | Default | Description |
|----------|-----------------|----------------------|------------------------------------------------------|
| title | string | "Untitled Notebook" | Notebook title |
| engine | string | "ngspice" | Simulation engine |
| tags | list of strings | [] | Searchable tags |
| cells | list of objects | (required) | Cells to create, each with `type` and `source` |
| run | bool | false | If true, execute each SPICE cell after creation |
**Response** `201` — Same shape as `POST /api/notebooks` (notebook with `id` at top level).
When `run` is `true`, each `spice` cell is executed sequentially using the notebook's engine. Simulation results are stored in each cell's `outputs` array. Non-SPICE cells are unaffected.
---
## Data Models
### CellType (enum)
`"markdown"` | `"spice"` | `"python"` | `"schematic"`
### Cell
```json
{
"id": "cell-a1b2c3d4e5f6",
"type": "spice",
"source": "V1 1 0 DC 5\n.op\n.end",
"outputs": [
{
"output_type": "simulation_result",
"data": { "success": true, "waveform": {...}, "log": "...", "error": null, "elapsed_seconds": 0.3 },
"timestamp": "2026-02-13T18:35:00+00:00"
}
]
}
```
### Notebook
```json
{
"spicebook_version": "2026-02-13",
"metadata": {
"title": "string",
"engine": "ngspice",
"tags": ["string"],
"created": "ISO-8601 datetime",
"modified": "ISO-8601 datetime"
},
"cells": [Cell, ...]
}
```
### SimulationResponse
```json
{
"success": true,
"waveform": WaveformData | null,
"log": "string",
"error": "string | null",
"elapsed_seconds": 0.0
}
```
### WaveformData
```json
{
"variables": [{"name": "v(out)", "type": "voltage"}],
"points": 100,
"x_data": [0.0, 0.001, ...],
"y_data": {"v(out)": [0.0, 0.5, ...]},
"x_type": "time",
"is_complex": false,
"y_magnitude_db": null,
"y_phase_deg": null
}
```
For AC analysis (`is_complex: true`), `y_magnitude_db` and `y_phase_deg` contain per-signal arrays. `x_type` will be `"frequency"` and `x_data` holds frequency values in Hz.
---
## SPICE Netlist Primer
SpiceBook supports **ngspice** and **LTspice** as simulation engines. Netlists are plain text describing a circuit and the analysis to perform.
### Basic structure
```spice
* Title line (optional comment)
V1 node_pos node_neg DC 5 * DC voltage source
R1 node_a node_b 1k * Resistor: 1 kilo-ohm
C1 node_a node_b 100n * Capacitor: 100 nanofarads
L1 node_a node_b 10m * Inductor: 10 millihenrys
.analysis_type parameters
.end
```
Node `0` is always ground.
### Supported analysis types
| Command | Description | Example |
|---------|-------------|---------|
| `.op` | DC operating point | `.op` |
| `.dc` | DC sweep | `.dc V1 0 5 0.1` |
| `.tran` | Transient (time-domain) | `.tran 1u 10m` |
| `.ac` | AC frequency sweep | `.ac dec 100 1 1meg` |
### Engineering suffixes
| Suffix | Multiplier | Example |
|--------|-----------|---------|
| T | 10^12 | `1T` = 1 tera |
| G | 10^9 | `2.2G` = 2.2 giga |
| meg | 10^6 | `1meg` = 1 mega (note: not `M` — that's milli in SPICE) |
| k | 10^3 | `4.7k` = 4700 |
| m | 10^-3 | `10m` = 0.01 |
| u | 10^-6 | `100u` = 100 micro |
| n | 10^-9 | `47n` = 47 nano |
| p | 10^-12 | `10p` = 10 pico |
| f | 10^-15 | `1f` = 1 femto |
### Common sources
```spice
V1 node+ node- DC 5 * DC voltage
V2 node+ node- AC 1 * AC source (for .ac analysis)
V3 node+ node- PULSE(0 5 0 1n 1n 5u 10u) * Pulse source
I1 node+ node- DC 1m * DC current source
```
---
## Example Workflow
### 1. Create a notebook with the compose endpoint
```bash
curl -X POST https://spicebook.warehack.ing/api/notebooks/compose \
-H "Content-Type: application/json" \
-d '{
"title": "Voltage Divider",
"engine": "ngspice",
"tags": ["resistive", "dc", "beginner"],
"cells": [
{
"type": "markdown",
"source": "# Voltage Divider\n\nTwo resistors divide a 5V supply."
},
{
"type": "spice",
"source": "V1 1 0 DC 5\nR1 1 2 1k\nR2 2 0 2k\n.op\n.end"
},
{
"type": "markdown",
"source": "## Expected Result\n\nV(2) = 5 * 2k / (1k + 2k) = 3.333V"
}
],
"run": true
}'
```
Response includes the notebook `id` and all cells. Because `run: true`, the SPICE cell's `outputs` array will contain the simulation result with `v(2) = 3.333`.
### 2. View the notebook in the browser
Open `https://spicebook.warehack.ing/notebook/{id}` — the notebook renders with the markdown cells formatted and the SPICE cell showing its simulation output.
### 3. Add another cell
```bash
curl -X POST https://spicebook.warehack.ing/api/notebooks/{id}/cells \
-H "Content-Type: application/json" \
-d '{
"type": "spice",
"source": "V1 1 0 DC 5\nR1 1 2 1k\nR2 2 0 2k\n.dc V1 0 10 0.5\n.end"
}'
```
### 4. Run the new cell
```bash
curl -X POST https://spicebook.warehack.ing/api/notebooks/{id}/cells/{cell_id}/run
```
### 5. Generate a schematic
```bash
curl -X POST https://spicebook.warehack.ing/api/notebooks/{id}/cells/{cell_id}/schematic
```
### 6. Visualize waveform data
Take the `waveform` from the simulation response and POST it to the waveform endpoint:
```bash
curl -X POST https://spicebook.warehack.ing/api/waveforms/svg/raw \
-H "Content-Type: application/json" \
-d '{
"waveform": { ...waveform object from simulation response... },
"title": "DC Sweep",
"signals": ["v(2)"]
}' \
-o plot.svg
```
---
## Health Check
```
GET /health
```
**Response** `200`
```json
{"status": "ok", "version": "2026.02.13"}
```
---
## Embedding
Any notebook can be embedded on a third-party page via iframe. No API key or authentication is required.
### Embed URL
```
https://spicebook.warehack.ing/embed/{notebook_id}?theme=dark
```
| Parameter | Values | Default | Description |
|-----------|--------|---------|-------------|
| theme | `dark`, `light` | `dark` | Initial color theme |
### iframe snippet
```html
<iframe
src="https://spicebook.warehack.ing/embed/{notebook_id}?theme=dark"
width="100%" height="600"
style="border: 1px solid #334155; border-radius: 8px;"
allow="clipboard-write"
></iframe>
```
The embed renders the notebook read-only with working simulation — viewers can run SPICE cells and see waveform results.
### Theme control via postMessage
The parent page can switch the embed's theme at runtime:
```javascript
iframe.contentWindow.postMessage(
{ type: 'spicebook-theme', theme: 'light' },
'*'
);
```
Accepted values for `theme`: `"dark"`, `"light"`. Other messages are ignored.
### Auto-resize via postMessage
The embed reports its content height to the parent page whenever the layout changes (initial render, simulation results, expanded cells). The parent listens for `spicebook-resize` messages and updates the iframe height:
```javascript
window.addEventListener('message', function(e) {
if (e.data && e.data.type === 'spicebook-resize') {
document.getElementById('spicebook-{notebook_id}').style.height = e.data.height + 'px';
}
});
```
The message payload:
```json
{ "type": "spicebook-resize", "height": 1842 }
```
`height` is `document.documentElement.scrollHeight` in pixels. The embed sends this message on every `ResizeObserver` callback, deduplicated to only fire when height actually changes. The **Embed** button's generated snippet includes this listener automatically.
### Discovering the embed snippet
In the notebook editor UI, the **Embed** button in the toolbar opens a popover with a ready-to-copy iframe snippet (including auto-resize listener) and a theme toggle.