@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 +16 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +192 -0
- package/package.json +33 -0
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
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|