Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/bun-formatcheck.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Created using @tscircuit/plop (npm install -g @tscircuit/plop)
name: Format Check

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
format-check:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun install

- name: Run format check
run: bun run format:check
70 changes: 70 additions & 0 deletions .github/workflows/bun-pver-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Created using @tscircuit/plop (npm install -g @tscircuit/plop)
name: Publish to npm
on:
push:
branches:
- main
workflow_dispatch:

env:
UPSTREAM_REPOS: "" # comma-separated list, e.g. "eval,tscircuit,docs"
UPSTREAM_PACKAGES_TO_UPDATE: "" # comma-separated list, e.g. "@tscircuit/core,@tscircuit/protos"

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }}
- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/setup-node@v3
with:
node-version: 20
registry-url: https://registry.npmjs.org/
- run: npm install -g pver
- run: bun install --frozen-lockfile
- run: bun run build
- run: pver release
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }}

# - name: Create Pull Request
# id: create-pr
# uses: peter-evans/create-pull-request@v5
# with:
# commit-message: "chore: bump version"
# title: "chore: bump version"
# body: "Automated package update"
# branch: bump-version-${{ github.run_number }}
# base: main
# token: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }}
# committer: tscircuitbot <[email protected]>
# author: tscircuitbot <[email protected]>

# - name: Enable auto-merge
# if: steps.create-pr.outputs.pull-request-number != ''
# run: |
# gh pr merge ${{ steps.create-pr.outputs.pull-request-number }} --rebase --delete-branch
# env:
# GH_TOKEN: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }}

# - name: Trigger upstream repo updates
# if: env.UPSTREAM_REPOS && env.UPSTREAM_PACKAGES_TO_UPDATE
# run: |
# IFS=',' read -ra REPOS <<< "${{ env.UPSTREAM_REPOS }}"
# for repo in "${REPOS[@]}"; do
# if [[ -n "$repo" ]]; then
# echo "Triggering update for repo: $repo"
# curl -X POST \
# -H "Accept: application/vnd.github.v3+json" \
# -H "Authorization: token ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }}" \
# -H "Content-Type: application/json" \
# "https://api.github.com/repos/tscircuit/$repo/actions/workflows/update-package.yml/dispatches" \
# -d "{\"ref\":\"main\",\"inputs\":{\"package_names\":\"${{ env.UPSTREAM_PACKAGES_TO_UPDATE }}\"}}"
# fi
# done
31 changes: 31 additions & 0 deletions .github/workflows/bun-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Created using @tscircuit/plop (npm install -g @tscircuit/plop)
name: Bun Test

on:
pull_request:
push:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 5

# Skip test for PRs that not chore: bump version
if: "${{ github.event_name != 'pull_request' || github.event.pull_request.title != 'chore: bump version' }}"

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun install

- name: Run tests
run: bun test
26 changes: 26 additions & 0 deletions .github/workflows/bun-typecheck.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Created using @tscircuit/plop (npm install -g @tscircuit/plop)
name: Type Check

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
type-check:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun i

- name: Run type check
run: bunx tsc --noEmit
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# spicey

Run [SPICE](https://en.wikipedia.org/wiki/SPICE) simulations in native javascript. An alternative to [ngspice](https://ngspice.sourceforge.io/)

[![npm version](https://img.shields.io/npm/v/spicey.svg)](https://www.npmjs.com/package/spicey)

```tsx
import { simulate, formatAcResult } from "spicey"

Expand Down Expand Up @@ -31,3 +35,20 @@ formatAcResult(result1.ac)
..."
`)
```

## Proposed directory structure

To make it easy to extend the simulator (for example to add transistor models later), the library is now organized into focused modules:

```
lib/
analysis/ # High-level simulation entry points (simulate, simulateAC, simulateTRAN)
constants/ # Shared numeric constants
formatting/ # Result formatting helpers
math/ # Numeric utilities such as Complex arithmetic and matrix solvers
parsing/ # Netlist parsing and circuit data structures
stamping/ # Matrix/RHS stamping helpers for modified nodal analysis
utils/ # Generic helpers (e.g., logarithmic sweeps)
```

Each exported function lives in its own file with a matching name, so new capabilities can be added without creating monolithic modules.
12 changes: 12 additions & 0 deletions lib/analysis/simulate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { parseNetlist } from "../parsing/parseNetlist"
import { simulateAC } from "./simulateAC"
import { simulateTRAN } from "./simulateTRAN"

function simulate(netlistText: string) {
const circuit = parseNetlist(netlistText)
const ac = simulateAC(circuit)
const tran = simulateTRAN(circuit)
return { circuit, ac, tran }
}

export { simulate }
114 changes: 114 additions & 0 deletions lib/analysis/simulateAC.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { EPS } from "../constants/EPS"
import { Complex } from "../math/Complex"
import { solveComplex } from "../math/solveComplex"
import type { ParsedCircuit } from "../parsing/parseNetlist"
import { logspace } from "../utils/logspace"
import { stampAdmittanceComplex } from "../stamping/stampAdmittanceComplex"
import { stampVoltageSourceComplex } from "../stamping/stampVoltageSourceComplex"

function simulateAC(ckt: ParsedCircuit) {
if (!ckt.analyses.ac) return null

const { mode, N, f1, f2 } = ckt.analyses.ac
const nNodeVars = ckt.nodes.count() - 1
const nVsrc = ckt.V.length
const Nvar = nNodeVars + nVsrc

const freqs =
mode === "dec"
? logspace(f1, f2, N)
: (() => {
const arr: number[] = []
const npts = Math.max(2, N)
const step = (f2 - f1) / (npts - 1)
for (let i = 0; i < npts; i++) arr.push(f1 + i * step)
return arr
})()

const nodeVoltages: Record<string, Complex[]> = {}
ckt.nodes.rev.forEach((name, id) => {
if (id !== 0) nodeVoltages[name] = []
})
const elementCurrents: Record<string, Complex[]> = {}

const twoPi = 2 * Math.PI

for (const f of freqs) {
const A = Array.from({ length: Nvar }, () =>
Array.from({ length: Nvar }, () => Complex.from(0, 0)),
)
const b = Array.from({ length: Nvar }, () => Complex.from(0, 0))

for (const r of ckt.R) {
if (r.R <= 0) throw new Error(`R ${r.name} must be > 0`)
const Y = Complex.from(1 / r.R, 0)
stampAdmittanceComplex(A, ckt.nodes, r.n1, r.n2, Y)
}

for (const c of ckt.C) {
const Y = Complex.from(0, twoPi * f * c.C)
stampAdmittanceComplex(A, ckt.nodes, c.n1, c.n2, Y)
}

for (const l of ckt.L) {
const denom = Complex.from(0, twoPi * f * l.L)
const Y =
denom.abs() < EPS ? Complex.from(0, 0) : Complex.from(1, 0).div(denom)
stampAdmittanceComplex(A, ckt.nodes, l.n1, l.n2, Y)
}

for (const vs of ckt.V) {
const Vph = Complex.fromPolar(vs.acMag || 0, vs.acPhaseDeg || 0)
stampVoltageSourceComplex(A, b, ckt.nodes, vs, Vph)
}

const x = solveComplex(A, b)

for (let id = 1; id < ckt.nodes.count(); id++) {
const idx = id - 1
const nodeName = ckt.nodes.rev[id]
if (!nodeName) continue
const series = nodeVoltages[nodeName]
if (!series) continue
series.push(x[idx] ?? Complex.from(0, 0))
}

for (const r of ckt.R) {
const v1 =
r.n1 === 0 ? Complex.from(0, 0) : (x[r.n1 - 1] ?? Complex.from(0, 0))
const v2 =
r.n2 === 0 ? Complex.from(0, 0) : (x[r.n2 - 1] ?? Complex.from(0, 0))
const Y = Complex.from(1 / r.R, 0)
const i = Y.mul(v1.sub(v2))
;(elementCurrents[r.name] ||= []).push(i)
}
for (const c of ckt.C) {
const v1 =
c.n1 === 0 ? Complex.from(0, 0) : (x[c.n1 - 1] ?? Complex.from(0, 0))
const v2 =
c.n2 === 0 ? Complex.from(0, 0) : (x[c.n2 - 1] ?? Complex.from(0, 0))
const Y = Complex.from(0, twoPi * f * c.C)
const i = Y.mul(v1.sub(v2))
;(elementCurrents[c.name] ||= []).push(i)
}
for (const l of ckt.L) {
const v1 =
l.n1 === 0 ? Complex.from(0, 0) : (x[l.n1 - 1] ?? Complex.from(0, 0))
const v2 =
l.n2 === 0 ? Complex.from(0, 0) : (x[l.n2 - 1] ?? Complex.from(0, 0))
const denom = Complex.from(0, twoPi * f * l.L)
const Y =
denom.abs() < EPS ? Complex.from(0, 0) : Complex.from(1, 0).div(denom)
const i = Y.mul(v1.sub(v2))
;(elementCurrents[l.name] ||= []).push(i)
}
for (const vs of ckt.V) {
const i = x[vs.index] ?? Complex.from(0, 0)
;(elementCurrents[vs.name] ||= []).push(i)
}
}

return { freqs, nodeVoltages, elementCurrents }
}

export { simulateAC }
Loading