@tscircuit/ngspice-spice-engine 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # @tscircuit/ngspice-spice-engine
2
+
3
+ A tscircuit-compatible SPICE engine using ngspice.
4
+
5
+ ```tsx
6
+ import ngspiceSpiceEngine from "@tscircuit/ngspice-spice-engine"
7
+ import { Circuit } from "tscircuit"
8
+
9
+ const circuit = new Circuit({
10
+ platform: {
11
+ spiceEngineMap: {
12
+ ngspice: ngspiceSpiceEngine
13
+ }
14
+ }
15
+ })
16
+ ```
@@ -0,0 +1,21 @@
1
+ import { SpiceEngine } from '@tscircuit/props';
2
+ import { ResultType } from 'eecircuit-engine';
3
+
4
+ interface TranParams {
5
+ tstep?: number;
6
+ tstop?: number;
7
+ tstart?: number;
8
+ tmax?: number;
9
+ uic?: boolean;
10
+ }
11
+ declare const parseTranParams: (spiceString: string) => TranParams | null;
12
+
13
+ interface VoltageGraph {
14
+ netName: string;
15
+ time: number[];
16
+ voltage: number[];
17
+ }
18
+ declare const eecircuitResultToVGraphs: (result: ResultType, spiceString: string) => VoltageGraph[];
19
+ declare const createNgspiceSpiceEngine: () => Promise<SpiceEngine>;
20
+
21
+ export { type TranParams, createNgspiceSpiceEngine, createNgspiceSpiceEngine as default, eecircuitResultToVGraphs, parseTranParams };
package/dist/index.js ADDED
@@ -0,0 +1,192 @@
1
+ // lib/parse-tran-params.ts
2
+ var SUFFIX_MULTIPLIERS = {
3
+ t: 1e12,
4
+ g: 1e9,
5
+ meg: 1e6,
6
+ k: 1e3,
7
+ m: 1e-3,
8
+ ms: 1e-3,
9
+ u: 1e-6,
10
+ us: 1e-6,
11
+ n: 1e-9,
12
+ ns: 1e-9,
13
+ p: 1e-12,
14
+ ps: 1e-12,
15
+ f: 1e-15,
16
+ fs: 1e-15,
17
+ s: 1
18
+ };
19
+ var sanitizeToken = (token) => token.replace(/[,]/g, "");
20
+ var parseNumericToken = (token) => {
21
+ const sanitized = sanitizeToken(token);
22
+ const normalized = sanitized.toLowerCase();
23
+ const match = normalized.match(/^([+-]?\d*\.?\d+(?:e[+-]?\d+)?)([a-z]+)?$/i);
24
+ if (!match) {
25
+ return void 0;
26
+ }
27
+ const [, basePart = "", suffix = ""] = match;
28
+ const base = Number.parseFloat(basePart);
29
+ if (Number.isNaN(base)) {
30
+ return void 0;
31
+ }
32
+ if (!suffix) {
33
+ return base;
34
+ }
35
+ const multiplier = SUFFIX_MULTIPLIERS[suffix] ?? SUFFIX_MULTIPLIERS[suffix.replace(/s$/, "")] ?? 1;
36
+ return base * multiplier;
37
+ };
38
+ var parseTranParams = (spiceString) => {
39
+ const lines = spiceString.split(/\r?\n/);
40
+ for (const rawLine of lines) {
41
+ const line = rawLine.trim();
42
+ if (!line || line.startsWith("*")) {
43
+ continue;
44
+ }
45
+ if (!line.toLowerCase().startsWith(".tran")) {
46
+ continue;
47
+ }
48
+ const [withoutComments = ""] = line.split(";");
49
+ const tokens = withoutComments.split(/\s+/).filter(Boolean);
50
+ if (tokens.length <= 1) {
51
+ return {};
52
+ }
53
+ const values = [];
54
+ let uic = false;
55
+ for (const token of tokens.slice(1)) {
56
+ if (token.toLowerCase() === "uic") {
57
+ uic = true;
58
+ continue;
59
+ }
60
+ const value = parseNumericToken(token);
61
+ if (value !== void 0) {
62
+ values.push(value);
63
+ }
64
+ }
65
+ const params = {};
66
+ if (values[0] !== void 0) {
67
+ params.tstep = values[0];
68
+ }
69
+ if (values[1] !== void 0) {
70
+ params.tstop = values[1];
71
+ }
72
+ if (values[2] !== void 0) {
73
+ params.tstart = values[2];
74
+ }
75
+ if (values[3] !== void 0) {
76
+ params.tmax = values[3];
77
+ }
78
+ if (uic) {
79
+ params.uic = true;
80
+ }
81
+ return params;
82
+ }
83
+ return null;
84
+ };
85
+
86
+ // lib/index.ts
87
+ var ensureSimulation = async () => {
88
+ const { Simulation: SimulationCtor } = await import("eecircuit-engine");
89
+ const instance = new SimulationCtor();
90
+ await instance.start();
91
+ return instance;
92
+ };
93
+ var simulationPromise = null;
94
+ var getSimulation = async () => {
95
+ if (!simulationPromise) {
96
+ simulationPromise = ensureSimulation().catch((error) => {
97
+ simulationPromise = null;
98
+ throw error;
99
+ });
100
+ }
101
+ return simulationPromise;
102
+ };
103
+ var extractRequestedPlots = (spiceString) => {
104
+ const match = spiceString.match(/\.print\s+tran\s+(.*)/i);
105
+ if (!match || !match[1]) {
106
+ return null;
107
+ }
108
+ const tokens = match[1].toLowerCase().split(/\s+/).filter(Boolean);
109
+ if (tokens.length === 0) {
110
+ return null;
111
+ }
112
+ return new Set(tokens);
113
+ };
114
+ var getNetName = (rawName) => {
115
+ const match = rawName.match(/^v\((.*)\)$/i);
116
+ if (!match) {
117
+ return rawName;
118
+ }
119
+ return match[1] ?? rawName;
120
+ };
121
+ var eecircuitResultToVGraphs = (result, spiceString) => {
122
+ if (!result || !result.data || result.dataType !== "real") {
123
+ return [];
124
+ }
125
+ const requestedPlots = extractRequestedPlots(spiceString);
126
+ const timeData = result.data.find((item) => item.type === "time");
127
+ if (!timeData || !Array.isArray(timeData.values)) {
128
+ return [];
129
+ }
130
+ const timeValues = timeData.values;
131
+ const graphs = [];
132
+ for (const item of result.data) {
133
+ if (item.type !== "voltage" || !Array.isArray(item.values)) {
134
+ continue;
135
+ }
136
+ if (requestedPlots && !requestedPlots.has(item.name.toLowerCase())) {
137
+ continue;
138
+ }
139
+ const netName = getNetName(item.name);
140
+ graphs.push({
141
+ netName,
142
+ time: timeValues,
143
+ voltage: item.values
144
+ });
145
+ }
146
+ return graphs;
147
+ };
148
+ var voltageGraphsToCircuitJson = (graphs, spiceString) => {
149
+ const tranParams = parseTranParams(spiceString);
150
+ return graphs.map((graph) => ({
151
+ type: "simulation_transient_voltage_graph",
152
+ simulation_experiment_id: "placeholder_simulation_experiment_id",
153
+ simulation_transient_voltage_graph_id: `simulation_graph_${graph.netName}`,
154
+ name: graph.netName,
155
+ voltage_levels: graph.voltage,
156
+ timestamps_ms: graph.time.map((timePoint) => timePoint * 1e3),
157
+ start_time_ms: (tranParams?.tstart ?? 0) * 1e3,
158
+ time_per_step: (tranParams?.tstep ?? 0) * 1e3,
159
+ end_time_ms: (tranParams?.tstop ?? 0) * 1e3
160
+ }));
161
+ };
162
+ var simulate = async (spiceString) => {
163
+ const simulation = await getSimulation();
164
+ simulation.setNetList(spiceString);
165
+ let result;
166
+ try {
167
+ result = await simulation.runSim();
168
+ } catch (error) {
169
+ console.error(error);
170
+ throw error;
171
+ }
172
+ if (!result) {
173
+ return { simulationResultCircuitJson: [] };
174
+ }
175
+ const graphs = eecircuitResultToVGraphs(result, spiceString);
176
+ return {
177
+ simulationResultCircuitJson: voltageGraphsToCircuitJson(
178
+ graphs,
179
+ spiceString
180
+ )
181
+ };
182
+ };
183
+ var createNgspiceSpiceEngine = async () => ({
184
+ simulate
185
+ });
186
+ var index_default = createNgspiceSpiceEngine;
187
+ export {
188
+ createNgspiceSpiceEngine,
189
+ index_default as default,
190
+ eecircuitResultToVGraphs,
191
+ parseTranParams
192
+ };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@tscircuit/ngspice-spice-engine",
3
+ "version": "0.0.1",
4
+ "description": "A tscircuit-compatible SPICE engine using ngspice.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "files": ["dist"],
8
+ "scripts": {
9
+ "build": "tsup-node ./lib/index.ts --dts --format esm",
10
+ "test": "bun test",
11
+ "test:watch": "bun test --watch",
12
+ "format": "biome format --write .",
13
+ "format:check": "biome format --diagnostic-level=error .",
14
+ "lint": "biome check .",
15
+ "typecheck": "tsc --noEmit"
16
+ },
17
+ "dependencies": {
18
+ "eecircuit-engine": "^1.5.6"
19
+ },
20
+ "devDependencies": {
21
+ "@tscircuit/props": "^0.0.374",
22
+ "circuit-json": "^0.0.290",
23
+ "@biomejs/biome": "^2.2.7",
24
+ "@types/bun": "latest",
25
+ "tscircuit": "^0.0.805",
26
+ "tsup": "^8.5.0"
27
+ },
28
+ "peerDependencies": {
29
+ "typescript": "^5",
30
+ "circuit-json": "*",
31
+ "@tscircuit/props": "*"
32
+ }
33
+ }