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.
89 lines
2.9 KiB
Python
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
|