Add /llms.txt API reference and POST /api/notebooks/compose endpoint
Machine-readable API docs at /llms.txt for LLM collaboration on circuit design notebooks linked from Mims Electronics Reference Library. Compose endpoint creates fully-populated notebooks in one call with optional SPICE simulation. Per-cell try/except ensures partial simulation failures don't lose the notebook. Also extracts get_engine to spicebook.engine and makes generate_notebook_id a public API.
This commit is contained in:
parent
c581786372
commit
1e08be4409
@ -0,0 +1,13 @@
|
|||||||
|
"""SPICE simulation engine registry."""
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
from spicebook.engine.base import SpiceEngine
|
||||||
|
from spicebook.engine.ngspice import NgspiceEngine
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine(engine_name: str) -> SpiceEngine:
|
||||||
|
"""Resolve a simulation engine by name."""
|
||||||
|
if engine_name == "ngspice":
|
||||||
|
return NgspiceEngine()
|
||||||
|
raise HTTPException(status_code=400, detail=f"Unsupported engine: '{engine_name}'")
|
||||||
@ -10,7 +10,7 @@ from fastapi import FastAPI, Request, Response
|
|||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from spicebook.config import settings
|
from spicebook.config import settings
|
||||||
from spicebook.routers import notebooks, schematics, simulation, waveforms
|
from spicebook.routers import compose, notebooks, schematics, simulation, waveforms
|
||||||
|
|
||||||
logger = logging.getLogger("spicebook")
|
logger = logging.getLogger("spicebook")
|
||||||
|
|
||||||
@ -59,6 +59,9 @@ def create_app() -> FastAPI:
|
|||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
# compose MUST be registered before notebooks so that
|
||||||
|
# POST /api/notebooks/compose matches before {notebook_id}
|
||||||
|
application.include_router(compose.router)
|
||||||
application.include_router(notebooks.router)
|
application.include_router(notebooks.router)
|
||||||
application.include_router(schematics.router)
|
application.include_router(schematics.router)
|
||||||
application.include_router(simulation.router)
|
application.include_router(simulation.router)
|
||||||
|
|||||||
@ -67,3 +67,16 @@ class UpdateCellRequest(BaseModel):
|
|||||||
|
|
||||||
class ReorderCellsRequest(BaseModel):
|
class ReorderCellsRequest(BaseModel):
|
||||||
cell_ids: list[str] # Ordered list of all cell IDs
|
cell_ids: list[str] # Ordered list of all cell IDs
|
||||||
|
|
||||||
|
|
||||||
|
class ComposeCellInput(BaseModel):
|
||||||
|
type: CellType
|
||||||
|
source: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class ComposeNotebookRequest(BaseModel):
|
||||||
|
title: str = Field("Untitled Notebook", max_length=256)
|
||||||
|
engine: str = "ngspice"
|
||||||
|
tags: list[str] = Field(default_factory=list, max_length=20)
|
||||||
|
cells: list[ComposeCellInput] = Field(..., max_length=100)
|
||||||
|
run: bool = False
|
||||||
|
|||||||
94
backend/src/spicebook/routers/compose.py
Normal file
94
backend/src/spicebook/routers/compose.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
"""Compose endpoint — create a fully-populated notebook in one call."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import tempfile
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
|
from spicebook.config import settings
|
||||||
|
from spicebook.engine import get_engine
|
||||||
|
from spicebook.models.notebook import (
|
||||||
|
Cell,
|
||||||
|
CellOutput,
|
||||||
|
CellType,
|
||||||
|
ComposeNotebookRequest,
|
||||||
|
Notebook,
|
||||||
|
NotebookMetadata,
|
||||||
|
)
|
||||||
|
from spicebook.models.simulation import SimulationResponse
|
||||||
|
from spicebook.storage.filesystem import generate_notebook_id, save_notebook
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/notebooks", tags=["compose"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/compose", status_code=201)
|
||||||
|
async def compose_notebook(req: ComposeNotebookRequest):
|
||||||
|
"""Create a notebook with pre-populated cells, optionally running SPICE cells."""
|
||||||
|
if not req.cells:
|
||||||
|
raise HTTPException(status_code=400, detail="At least one cell is required")
|
||||||
|
|
||||||
|
nb_id = generate_notebook_id(req.title)
|
||||||
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
cells: list[Cell] = []
|
||||||
|
for item in req.cells:
|
||||||
|
cells.append(
|
||||||
|
Cell(
|
||||||
|
id=f"cell-{uuid.uuid4().hex[:12]}",
|
||||||
|
type=item.type,
|
||||||
|
source=item.source,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
notebook = Notebook(
|
||||||
|
metadata=NotebookMetadata(
|
||||||
|
title=req.title,
|
||||||
|
engine=req.engine,
|
||||||
|
tags=req.tags,
|
||||||
|
created=now,
|
||||||
|
modified=now,
|
||||||
|
),
|
||||||
|
cells=cells,
|
||||||
|
)
|
||||||
|
|
||||||
|
if req.run:
|
||||||
|
engine = get_engine(req.engine)
|
||||||
|
for cell in notebook.cells:
|
||||||
|
if cell.type != CellType.SPICE:
|
||||||
|
continue
|
||||||
|
if not cell.source.strip():
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
with tempfile.TemporaryDirectory(prefix="spicebook-sim-") as tmpdir:
|
||||||
|
result = await engine.run(cell.source, Path(tmpdir))
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Simulation failed for cell %s", cell.id)
|
||||||
|
result = SimulationResponse(
|
||||||
|
success=False,
|
||||||
|
error="Internal simulation error",
|
||||||
|
elapsed_seconds=0.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
run_ts = datetime.now(timezone.utc).isoformat()
|
||||||
|
cell.outputs.append(
|
||||||
|
CellOutput(
|
||||||
|
output_type="simulation_result" if result.success else "error",
|
||||||
|
data={
|
||||||
|
"success": result.success,
|
||||||
|
"waveform": result.waveform.model_dump() if result.waveform else None,
|
||||||
|
"log": result.log,
|
||||||
|
"error": result.error,
|
||||||
|
"elapsed_seconds": result.elapsed_seconds,
|
||||||
|
},
|
||||||
|
timestamp=run_ts,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
save_notebook(settings.notebook_dir, nb_id, notebook)
|
||||||
|
return {"id": nb_id, **notebook.model_dump()}
|
||||||
@ -7,7 +7,7 @@ from pathlib import Path
|
|||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
from spicebook.config import settings
|
from spicebook.config import settings
|
||||||
from spicebook.engine.ngspice import NgspiceEngine
|
from spicebook.engine import get_engine
|
||||||
from spicebook.models.notebook import CellOutput, CellType
|
from spicebook.models.notebook import CellOutput, CellType
|
||||||
from spicebook.models.simulation import SimulationRequest, SimulationResponse
|
from spicebook.models.simulation import SimulationRequest, SimulationResponse
|
||||||
from spicebook.storage.filesystem import load_notebook, save_notebook
|
from spicebook.storage.filesystem import load_notebook, save_notebook
|
||||||
@ -15,17 +15,10 @@ from spicebook.storage.filesystem import load_notebook, save_notebook
|
|||||||
router = APIRouter(prefix="/api", tags=["simulation"])
|
router = APIRouter(prefix="/api", tags=["simulation"])
|
||||||
|
|
||||||
|
|
||||||
def _get_engine(engine_name: str) -> NgspiceEngine:
|
|
||||||
"""Resolve engine by name. Only ngspice is supported in Phase 1."""
|
|
||||||
if engine_name == "ngspice":
|
|
||||||
return NgspiceEngine()
|
|
||||||
raise HTTPException(status_code=400, detail=f"Unsupported engine: '{engine_name}'")
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/simulate", response_model=SimulationResponse)
|
@router.post("/simulate", response_model=SimulationResponse)
|
||||||
async def simulate(req: SimulationRequest):
|
async def simulate(req: SimulationRequest):
|
||||||
"""Run a standalone SPICE simulation."""
|
"""Run a standalone SPICE simulation."""
|
||||||
engine = _get_engine(req.engine)
|
engine = get_engine(req.engine)
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(prefix="spicebook-sim-") as tmpdir:
|
with tempfile.TemporaryDirectory(prefix="spicebook-sim-") as tmpdir:
|
||||||
result = await engine.run(req.netlist, Path(tmpdir))
|
result = await engine.run(req.netlist, Path(tmpdir))
|
||||||
@ -61,7 +54,7 @@ async def run_cell(notebook_id: str, cell_id: str):
|
|||||||
if not cell.source.strip():
|
if not cell.source.strip():
|
||||||
raise HTTPException(status_code=400, detail="Cell source is empty")
|
raise HTTPException(status_code=400, detail="Cell source is empty")
|
||||||
|
|
||||||
engine = _get_engine(nb.metadata.engine)
|
engine = get_engine(nb.metadata.engine)
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(prefix="spicebook-sim-") as tmpdir:
|
with tempfile.TemporaryDirectory(prefix="spicebook-sim-") as tmpdir:
|
||||||
result = await engine.run(cell.source, Path(tmpdir))
|
result = await engine.run(cell.source, Path(tmpdir))
|
||||||
|
|||||||
@ -33,7 +33,7 @@ def _slugify(text: str) -> str:
|
|||||||
return slug or "notebook"
|
return slug or "notebook"
|
||||||
|
|
||||||
|
|
||||||
def _generate_notebook_id(title: str) -> str:
|
def generate_notebook_id(title: str) -> str:
|
||||||
"""Generate a notebook ID from title, with short UUID suffix for uniqueness."""
|
"""Generate a notebook ID from title, with short UUID suffix for uniqueness."""
|
||||||
slug = _slugify(title)
|
slug = _slugify(title)
|
||||||
short_uid = uuid.uuid4().hex[:8]
|
short_uid = uuid.uuid4().hex[:8]
|
||||||
@ -105,7 +105,7 @@ def save_notebook(directory: Path, notebook_id: str, notebook: Notebook) -> None
|
|||||||
|
|
||||||
def create_notebook(directory: Path, title: str, engine: str = "ngspice") -> tuple[str, Notebook]:
|
def create_notebook(directory: Path, title: str, engine: str = "ngspice") -> tuple[str, Notebook]:
|
||||||
"""Create a new notebook and save it. Returns (notebook_id, notebook)."""
|
"""Create a new notebook and save it. Returns (notebook_id, notebook)."""
|
||||||
nb_id = _generate_notebook_id(title)
|
nb_id = generate_notebook_id(title)
|
||||||
now = _now_iso()
|
now = _now_iso()
|
||||||
|
|
||||||
notebook = Notebook(
|
notebook = Notebook(
|
||||||
|
|||||||
546
frontend/public/llms.txt
Normal file
546
frontend/public/llms.txt
Normal file
@ -0,0 +1,546 @@
|
|||||||
|
# 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. Powered by ngspice.
|
||||||
|
|
||||||
|
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"`.
|
||||||
|
|
||||||
|
**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"}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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 uses **ngspice** as its simulation engine. 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"}
|
||||||
|
```
|
||||||
Loading…
x
Reference in New Issue
Block a user