Ryan Malloy de7a29c69e Add auto-schematic generation from SPICE netlists
Parse netlists into component graphs and render circuit diagrams via
SchemDraw. Two layout strategies: loop layout for simple 2-terminal
circuits (RC, RL, voltage divider) and labeled grid for complex
circuits with active devices (BJT amplifiers, MOSFET).

Backend: netlist parser, schematic engine, POST API endpoint.
Frontend: SchematicViewer with zoom/download, stacked cell layout
showing schematic + SPICE editor + waveform simultaneously.
2026-02-13 06:07:30 -07:00

89 lines
2.9 KiB
Python

"""Simulation execution endpoints."""
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from fastapi import APIRouter, HTTPException
from spicebook.config import settings
from spicebook.engine.ngspice import NgspiceEngine
from spicebook.models.notebook import CellOutput, CellType
from spicebook.models.simulation import SimulationRequest, SimulationResponse
from spicebook.storage.filesystem import load_notebook, save_notebook
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)
async def simulate(req: SimulationRequest):
"""Run a standalone SPICE simulation."""
engine = _get_engine(req.engine)
with tempfile.TemporaryDirectory(prefix="spicebook-sim-") as tmpdir:
result = await engine.run(req.netlist, Path(tmpdir))
return result
@router.post(
"/notebooks/{notebook_id}/cells/{cell_id}/run",
response_model=SimulationResponse,
)
async def run_cell(notebook_id: str, cell_id: str):
"""Run a SPICE cell within a notebook and save the output."""
nb = load_notebook(settings.notebook_dir, notebook_id)
if nb is None:
raise HTTPException(status_code=404, detail=f"Notebook '{notebook_id}' not found")
cell = None
for c in nb.cells:
if c.id == cell_id:
cell = c
break
if cell is None:
raise HTTPException(status_code=404, detail=f"Cell '{cell_id}' not found")
if cell.type != CellType.SPICE:
raise HTTPException(
status_code=400,
detail=f"Cell is type '{cell.type.value}', not 'spice'",
)
if not cell.source.strip():
raise HTTPException(status_code=400, detail="Cell source is empty")
engine = _get_engine(nb.metadata.engine)
with tempfile.TemporaryDirectory(prefix="spicebook-sim-") as tmpdir:
result = await engine.run(cell.source, Path(tmpdir))
# Save output to the cell (preserve schematic outputs across re-runs)
now_iso = datetime.now(timezone.utc).isoformat()
sim_output = 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=now_iso,
)
preserved = [o for o in cell.outputs if o.output_type == "schematic"]
cell.outputs = [sim_output] + preserved
save_notebook(settings.notebook_dir, notebook_id, nb)
return result