@tscircuit/schematic-viewer 2.0.24 → 2.0.26
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/bun.lockb +0 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +582 -22
- package/dist/index.js.map +1 -1
- package/dist/workers/spice-simulation.worker.js +1 -0
- package/examples/example9-spice-simulation.fixture.tsx +77 -0
- package/lib/components/SchematicViewer.tsx +69 -13
- package/lib/components/SpiceIcon.tsx +14 -0
- package/lib/components/SpicePlot.tsx +193 -0
- package/lib/components/SpiceSimulationIcon.tsx +31 -0
- package/lib/components/SpiceSimulationOverlay.tsx +121 -0
- package/lib/hooks/useSpiceSimulation.ts +151 -0
- package/lib/types/eecircuit-engine.d.ts +147 -0
- package/lib/utils/spice-utils.ts +81 -0
- package/lib/utils/z-index-map.ts +1 -0
- package/lib/workers/spice-simulation.worker.ts +51 -0
- package/package.json +11 -10
- package/scripts/build-worker-blob-url.ts +55 -0
- package/tsup-webworker.config.ts +13 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Chart as ChartJS,
|
|
3
|
+
type ChartOptions,
|
|
4
|
+
CategoryScale,
|
|
5
|
+
LinearScale,
|
|
6
|
+
PointElement,
|
|
7
|
+
LineElement,
|
|
8
|
+
Title,
|
|
9
|
+
Tooltip,
|
|
10
|
+
Legend,
|
|
11
|
+
} from "chart.js"
|
|
12
|
+
import { Line } from "react-chartjs-2"
|
|
13
|
+
import type { PlotPoint } from "../hooks/useSpiceSimulation"
|
|
14
|
+
|
|
15
|
+
ChartJS.register(
|
|
16
|
+
CategoryScale,
|
|
17
|
+
LinearScale,
|
|
18
|
+
PointElement,
|
|
19
|
+
LineElement,
|
|
20
|
+
Title,
|
|
21
|
+
Tooltip,
|
|
22
|
+
Legend,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const colors = ["#8884d8", "#82ca9d", "#ffc658", "#ff7300", "#387908"]
|
|
26
|
+
|
|
27
|
+
const formatTimeWithUnits = (seconds: number) => {
|
|
28
|
+
if (seconds === 0) return "0s"
|
|
29
|
+
const absSeconds = Math.abs(seconds)
|
|
30
|
+
|
|
31
|
+
let unit = "s"
|
|
32
|
+
let scale = 1
|
|
33
|
+
if (absSeconds < 1e-12) {
|
|
34
|
+
unit = "fs"
|
|
35
|
+
scale = 1e15
|
|
36
|
+
} else if (absSeconds < 1e-9) {
|
|
37
|
+
unit = "ps"
|
|
38
|
+
scale = 1e12
|
|
39
|
+
} else if (absSeconds < 1e-6) {
|
|
40
|
+
unit = "ns"
|
|
41
|
+
scale = 1e9
|
|
42
|
+
} else if (absSeconds < 1e-3) {
|
|
43
|
+
unit = "us"
|
|
44
|
+
scale = 1e6
|
|
45
|
+
} else if (absSeconds < 1) {
|
|
46
|
+
unit = "ms"
|
|
47
|
+
scale = 1e3
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return `${parseFloat((seconds * scale).toPrecision(3))}${unit}`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const SpicePlot = ({
|
|
54
|
+
plotData,
|
|
55
|
+
nodes,
|
|
56
|
+
isLoading,
|
|
57
|
+
error,
|
|
58
|
+
}: {
|
|
59
|
+
plotData: PlotPoint[]
|
|
60
|
+
nodes: string[]
|
|
61
|
+
isLoading: boolean
|
|
62
|
+
error: string | null
|
|
63
|
+
}) => {
|
|
64
|
+
if (isLoading) {
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
style={{
|
|
68
|
+
height: "300px",
|
|
69
|
+
width: "100%",
|
|
70
|
+
display: "flex",
|
|
71
|
+
alignItems: "center",
|
|
72
|
+
justifyContent: "center",
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
Running simulation...
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (error) {
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
style={{
|
|
84
|
+
height: "300px",
|
|
85
|
+
width: "100%",
|
|
86
|
+
display: "flex",
|
|
87
|
+
alignItems: "center",
|
|
88
|
+
justifyContent: "center",
|
|
89
|
+
color: "red",
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
Error: {error}
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (plotData.length === 0) {
|
|
98
|
+
return (
|
|
99
|
+
<div
|
|
100
|
+
style={{
|
|
101
|
+
height: "300px",
|
|
102
|
+
width: "100%",
|
|
103
|
+
display: "flex",
|
|
104
|
+
alignItems: "center",
|
|
105
|
+
justifyContent: "center",
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
No data to plot. Check simulation output or SPICE netlist.
|
|
109
|
+
</div>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const chartData = {
|
|
114
|
+
datasets: nodes.map((node, i) => ({
|
|
115
|
+
label: node,
|
|
116
|
+
data: plotData.map((p) => ({
|
|
117
|
+
x: Number(p.name),
|
|
118
|
+
y: p[node] as number,
|
|
119
|
+
})),
|
|
120
|
+
borderColor: colors[i % colors.length],
|
|
121
|
+
backgroundColor: colors[i % colors.length],
|
|
122
|
+
fill: false,
|
|
123
|
+
tension: 0.1,
|
|
124
|
+
})),
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const options: ChartOptions<"line"> = {
|
|
128
|
+
responsive: true,
|
|
129
|
+
maintainAspectRatio: false,
|
|
130
|
+
plugins: {
|
|
131
|
+
legend: {
|
|
132
|
+
position: "top" as const,
|
|
133
|
+
labels: {
|
|
134
|
+
font: {
|
|
135
|
+
family: "sans-serif",
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
title: {
|
|
140
|
+
display: false,
|
|
141
|
+
},
|
|
142
|
+
tooltip: {
|
|
143
|
+
callbacks: {
|
|
144
|
+
title: (tooltipItems) => {
|
|
145
|
+
if (tooltipItems.length > 0) {
|
|
146
|
+
const item = tooltipItems[0]
|
|
147
|
+
return formatTimeWithUnits(item.parsed.x)
|
|
148
|
+
}
|
|
149
|
+
return ""
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
scales: {
|
|
155
|
+
x: {
|
|
156
|
+
type: "linear",
|
|
157
|
+
title: {
|
|
158
|
+
display: true,
|
|
159
|
+
text: "Time",
|
|
160
|
+
font: {
|
|
161
|
+
family: "sans-serif",
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
ticks: {
|
|
165
|
+
callback: (value) => formatTimeWithUnits(value as number),
|
|
166
|
+
font: {
|
|
167
|
+
family: "sans-serif",
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
y: {
|
|
172
|
+
title: {
|
|
173
|
+
display: true,
|
|
174
|
+
text: "Voltage",
|
|
175
|
+
font: {
|
|
176
|
+
family: "sans-serif",
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
ticks: {
|
|
180
|
+
font: {
|
|
181
|
+
family: "sans-serif",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<div style={{ position: "relative", height: "300px", width: "100%" }}>
|
|
190
|
+
<Line options={options} data={chartData} />
|
|
191
|
+
</div>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { SpiceIcon } from "./SpiceIcon"
|
|
2
|
+
import { zIndexMap } from "../utils/z-index-map"
|
|
3
|
+
|
|
4
|
+
export const SpiceSimulationIcon = ({
|
|
5
|
+
onClick,
|
|
6
|
+
}: {
|
|
7
|
+
onClick: () => void
|
|
8
|
+
}) => {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
onClick={onClick}
|
|
12
|
+
style={{
|
|
13
|
+
position: "absolute",
|
|
14
|
+
top: "16px",
|
|
15
|
+
right: "56px",
|
|
16
|
+
backgroundColor: "#fff",
|
|
17
|
+
color: "#000",
|
|
18
|
+
padding: "8px",
|
|
19
|
+
borderRadius: "4px",
|
|
20
|
+
cursor: "pointer",
|
|
21
|
+
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
|
|
22
|
+
display: "flex",
|
|
23
|
+
alignItems: "center",
|
|
24
|
+
gap: "4px",
|
|
25
|
+
zIndex: zIndexMap.spiceSimulationIcon,
|
|
26
|
+
}}
|
|
27
|
+
>
|
|
28
|
+
<SpiceIcon />
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { SpicePlot } from "./SpicePlot"
|
|
2
|
+
import type { PlotPoint } from "../hooks/useSpiceSimulation"
|
|
3
|
+
|
|
4
|
+
interface SpiceSimulationOverlayProps {
|
|
5
|
+
spiceString: string | null
|
|
6
|
+
onClose: () => void
|
|
7
|
+
plotData: PlotPoint[]
|
|
8
|
+
nodes: string[]
|
|
9
|
+
isLoading: boolean
|
|
10
|
+
error: string | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const SpiceSimulationOverlay = ({
|
|
14
|
+
spiceString,
|
|
15
|
+
onClose,
|
|
16
|
+
plotData,
|
|
17
|
+
nodes,
|
|
18
|
+
isLoading,
|
|
19
|
+
error,
|
|
20
|
+
}: SpiceSimulationOverlayProps) => {
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
style={{
|
|
24
|
+
position: "fixed",
|
|
25
|
+
top: 0,
|
|
26
|
+
left: 0,
|
|
27
|
+
right: 0,
|
|
28
|
+
bottom: 0,
|
|
29
|
+
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
30
|
+
display: "flex",
|
|
31
|
+
alignItems: "center",
|
|
32
|
+
justifyContent: "center",
|
|
33
|
+
zIndex: 1002,
|
|
34
|
+
fontFamily: "sans-serif",
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
<div
|
|
38
|
+
style={{
|
|
39
|
+
backgroundColor: "white",
|
|
40
|
+
padding: "24px",
|
|
41
|
+
borderRadius: "12px",
|
|
42
|
+
width: "90%",
|
|
43
|
+
maxWidth: "900px",
|
|
44
|
+
boxShadow: "0 4px 20px rgba(0, 0, 0, 0.15)",
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
<div
|
|
48
|
+
style={{
|
|
49
|
+
display: "flex",
|
|
50
|
+
justifyContent: "space-between",
|
|
51
|
+
alignItems: "center",
|
|
52
|
+
marginBottom: "24px",
|
|
53
|
+
borderBottom: "1px solid #eee",
|
|
54
|
+
paddingBottom: "16px",
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
<h2
|
|
58
|
+
style={{
|
|
59
|
+
margin: 0,
|
|
60
|
+
fontSize: "22px",
|
|
61
|
+
fontWeight: 600,
|
|
62
|
+
color: "#333",
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
SPICE Simulation
|
|
66
|
+
</h2>
|
|
67
|
+
<button
|
|
68
|
+
onClick={onClose}
|
|
69
|
+
style={{
|
|
70
|
+
background: "none",
|
|
71
|
+
border: "none",
|
|
72
|
+
fontSize: "28px",
|
|
73
|
+
cursor: "pointer",
|
|
74
|
+
color: "#888",
|
|
75
|
+
padding: 0,
|
|
76
|
+
lineHeight: 1,
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
×
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
<div>
|
|
83
|
+
<SpicePlot
|
|
84
|
+
plotData={plotData}
|
|
85
|
+
nodes={nodes}
|
|
86
|
+
isLoading={isLoading}
|
|
87
|
+
error={error}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
<div style={{ marginTop: "24px" }}>
|
|
91
|
+
<h3
|
|
92
|
+
style={{
|
|
93
|
+
marginTop: 0,
|
|
94
|
+
marginBottom: "12px",
|
|
95
|
+
fontSize: "18px",
|
|
96
|
+
fontWeight: 600,
|
|
97
|
+
color: "#333",
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
SPICE Netlist
|
|
101
|
+
</h3>
|
|
102
|
+
<pre
|
|
103
|
+
style={{
|
|
104
|
+
backgroundColor: "#fafafa",
|
|
105
|
+
padding: "16px",
|
|
106
|
+
borderRadius: "6px",
|
|
107
|
+
maxHeight: "150px",
|
|
108
|
+
overflowY: "auto",
|
|
109
|
+
border: "1px solid #eee",
|
|
110
|
+
color: "#333",
|
|
111
|
+
fontSize: "13px",
|
|
112
|
+
fontFamily: "monospace",
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
{spiceString}
|
|
116
|
+
</pre>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { useState, useEffect } from "react"
|
|
2
|
+
import type * as EecircuitEngine from "../types/eecircuit-engine"
|
|
3
|
+
// @ts-ignore
|
|
4
|
+
import { getSpiceSimulationWorkerBlobUrl } from "../workers/spice-simulation.worker.blob.js"
|
|
5
|
+
|
|
6
|
+
// Types from eecircuit-engine interface
|
|
7
|
+
type RealDataType = {
|
|
8
|
+
name: string
|
|
9
|
+
type: string
|
|
10
|
+
values: number[]
|
|
11
|
+
}
|
|
12
|
+
type ComplexNumber = {
|
|
13
|
+
real: number
|
|
14
|
+
img: number
|
|
15
|
+
}
|
|
16
|
+
type ComplexDataType = {
|
|
17
|
+
name: string
|
|
18
|
+
type: string
|
|
19
|
+
values: ComplexNumber[]
|
|
20
|
+
}
|
|
21
|
+
type EecEngineResult =
|
|
22
|
+
| {
|
|
23
|
+
header: string
|
|
24
|
+
numVariables: number
|
|
25
|
+
variableNames: string[]
|
|
26
|
+
numPoints: number
|
|
27
|
+
dataType: "real"
|
|
28
|
+
data: RealDataType[]
|
|
29
|
+
}
|
|
30
|
+
| {
|
|
31
|
+
header: string
|
|
32
|
+
numVariables: number
|
|
33
|
+
variableNames: string[]
|
|
34
|
+
numPoints: number
|
|
35
|
+
dataType: "complex"
|
|
36
|
+
data: ComplexDataType[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PlotPoint {
|
|
40
|
+
name: string // time or sweep variable
|
|
41
|
+
[key: string]: number | string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const parseEecEngineOutput = (
|
|
45
|
+
result: EecEngineResult,
|
|
46
|
+
): { plotData: PlotPoint[]; nodes: string[] } => {
|
|
47
|
+
const columnData: Record<string, number[]> = {}
|
|
48
|
+
|
|
49
|
+
if (result.dataType === "real") {
|
|
50
|
+
result.data.forEach((col) => {
|
|
51
|
+
columnData[col.name] = col.values
|
|
52
|
+
})
|
|
53
|
+
} else if (result.dataType === "complex") {
|
|
54
|
+
result.data.forEach((col) => {
|
|
55
|
+
// For now, plot the real part of complex numbers
|
|
56
|
+
columnData[col.name] = col.values.map((v) => v.real)
|
|
57
|
+
})
|
|
58
|
+
} else {
|
|
59
|
+
throw new Error("Unsupported data type in simulation result")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const timeKey = Object.keys(columnData).find(
|
|
63
|
+
(k) => k.toLowerCase() === "time" || k.toLowerCase() === "frequency",
|
|
64
|
+
)
|
|
65
|
+
if (!timeKey) {
|
|
66
|
+
throw new Error("No time or frequency data in simulation result")
|
|
67
|
+
}
|
|
68
|
+
const timeValues = columnData[timeKey]
|
|
69
|
+
const probedVariables = Object.keys(columnData).filter((k) => k !== timeKey)
|
|
70
|
+
const plotableNodes = probedVariables.map((n) =>
|
|
71
|
+
n.replace(/v\(([^)]+)\)/i, "$1"),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const plotData: PlotPoint[] = timeValues.map((t: number, i: number) => {
|
|
75
|
+
const point: PlotPoint = { name: t.toExponential(2) }
|
|
76
|
+
probedVariables.forEach((variable, j) => {
|
|
77
|
+
point[plotableNodes[j]] = columnData[variable][i]
|
|
78
|
+
})
|
|
79
|
+
return point
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return { plotData, nodes: plotableNodes }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type WorkerMessage =
|
|
86
|
+
| {
|
|
87
|
+
type: "result"
|
|
88
|
+
result: EecEngineResult
|
|
89
|
+
}
|
|
90
|
+
| { type: "error"; error: string }
|
|
91
|
+
|
|
92
|
+
export const useSpiceSimulation = (spiceString: string | null) => {
|
|
93
|
+
const [plotData, setPlotData] = useState<PlotPoint[]>([])
|
|
94
|
+
const [nodes, setNodes] = useState<string[]>([])
|
|
95
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
96
|
+
const [error, setError] = useState<string | null>(null)
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!spiceString) {
|
|
100
|
+
setIsLoading(false)
|
|
101
|
+
setPlotData([])
|
|
102
|
+
setNodes([])
|
|
103
|
+
setError(null)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
setIsLoading(true)
|
|
107
|
+
setError(null)
|
|
108
|
+
setPlotData([])
|
|
109
|
+
setNodes([])
|
|
110
|
+
|
|
111
|
+
const workerUrl = getSpiceSimulationWorkerBlobUrl()
|
|
112
|
+
|
|
113
|
+
if (!workerUrl) {
|
|
114
|
+
setError("Could not create SPICE simulation worker.")
|
|
115
|
+
setIsLoading(false)
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const worker = new Worker(workerUrl, { type: "module" })
|
|
120
|
+
|
|
121
|
+
worker.onmessage = (event: MessageEvent<WorkerMessage>) => {
|
|
122
|
+
if (event.data.type === "result") {
|
|
123
|
+
try {
|
|
124
|
+
const { plotData: parsedData, nodes: parsedNodes } =
|
|
125
|
+
parseEecEngineOutput(event.data.result)
|
|
126
|
+
setPlotData(parsedData)
|
|
127
|
+
setNodes(parsedNodes)
|
|
128
|
+
} catch (e: any) {
|
|
129
|
+
setError(e.message || "Failed to parse simulation result")
|
|
130
|
+
console.error(e)
|
|
131
|
+
}
|
|
132
|
+
} else if (event.data.type === "error") {
|
|
133
|
+
setError(event.data.error)
|
|
134
|
+
}
|
|
135
|
+
setIsLoading(false)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
worker.onerror = (err) => {
|
|
139
|
+
setError(err.message)
|
|
140
|
+
setIsLoading(false)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
worker.postMessage({ spiceString })
|
|
144
|
+
|
|
145
|
+
return () => {
|
|
146
|
+
worker.terminate()
|
|
147
|
+
}
|
|
148
|
+
}, [spiceString])
|
|
149
|
+
|
|
150
|
+
return { plotData, nodes, isLoading, error }
|
|
151
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
export declare type ComplexDataType = {
|
|
2
|
+
name: string
|
|
3
|
+
|
|
4
|
+
type: string
|
|
5
|
+
|
|
6
|
+
values: ComplexNumber[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export declare type ComplexNumber = {
|
|
10
|
+
real: number
|
|
11
|
+
|
|
12
|
+
img: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
|
|
17
|
+
* Read output from spice
|
|
18
|
+
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export declare type RealDataType = {
|
|
22
|
+
name: string
|
|
23
|
+
|
|
24
|
+
type: string
|
|
25
|
+
|
|
26
|
+
values: RealNumber[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export declare type RealNumber = number
|
|
30
|
+
|
|
31
|
+
export declare type ResultType =
|
|
32
|
+
| {
|
|
33
|
+
header: string
|
|
34
|
+
|
|
35
|
+
numVariables: number
|
|
36
|
+
|
|
37
|
+
variableNames: string[]
|
|
38
|
+
|
|
39
|
+
numPoints: number
|
|
40
|
+
|
|
41
|
+
dataType: "real"
|
|
42
|
+
|
|
43
|
+
data: RealDataType[]
|
|
44
|
+
}
|
|
45
|
+
| {
|
|
46
|
+
header: string
|
|
47
|
+
|
|
48
|
+
numVariables: number
|
|
49
|
+
|
|
50
|
+
variableNames: string[]
|
|
51
|
+
|
|
52
|
+
numPoints: number
|
|
53
|
+
|
|
54
|
+
dataType: "complex"
|
|
55
|
+
|
|
56
|
+
data: ComplexDataType[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export declare class Simulation {
|
|
60
|
+
private pass
|
|
61
|
+
|
|
62
|
+
private commandList
|
|
63
|
+
|
|
64
|
+
private cmd
|
|
65
|
+
|
|
66
|
+
private dataRaw
|
|
67
|
+
|
|
68
|
+
private results
|
|
69
|
+
|
|
70
|
+
private output
|
|
71
|
+
|
|
72
|
+
private info
|
|
73
|
+
|
|
74
|
+
private initInfo
|
|
75
|
+
|
|
76
|
+
private error
|
|
77
|
+
|
|
78
|
+
private initialized
|
|
79
|
+
|
|
80
|
+
private netList
|
|
81
|
+
|
|
82
|
+
private initPromiseResolve
|
|
83
|
+
|
|
84
|
+
private runPromiseResolve
|
|
85
|
+
|
|
86
|
+
private getInput
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
|
|
90
|
+
* Internal startup method that sets up the Module and simulation loop.
|
|
91
|
+
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
private startInternal
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
|
|
98
|
+
* Public start method.
|
|
99
|
+
|
|
100
|
+
* Returns a promise that resolves when the simulation module is initialized.
|
|
101
|
+
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
start: () => Promise<void>
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
|
|
108
|
+
* Triggers a simulation run and returns a promise that resolves with the results.
|
|
109
|
+
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
runSim: () => Promise<ResultType>
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
|
|
116
|
+
* Waits for a new simulation trigger.
|
|
117
|
+
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
private waitForNextRun
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
|
|
124
|
+
* Resolves the waiting promise to continue the simulation loop.
|
|
125
|
+
|
|
126
|
+
*/
|
|
127
|
+
|
|
128
|
+
private continueRun
|
|
129
|
+
|
|
130
|
+
private outputEvent
|
|
131
|
+
|
|
132
|
+
setNetList: (input: string) => void
|
|
133
|
+
|
|
134
|
+
private setOutputEvent
|
|
135
|
+
|
|
136
|
+
getInfo: () => string
|
|
137
|
+
|
|
138
|
+
getInitInfo: () => string
|
|
139
|
+
|
|
140
|
+
getError: () => string[]
|
|
141
|
+
|
|
142
|
+
isInitialized: () => boolean
|
|
143
|
+
|
|
144
|
+
private log_debug
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export {}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { circuitJsonToSpice } from "circuit-json-to-spice"
|
|
2
|
+
import type { CircuitJson } from "circuit-json"
|
|
3
|
+
|
|
4
|
+
export const getSpiceFromCircuitJson = (circuitJson: CircuitJson): string => {
|
|
5
|
+
const spiceNetlist = circuitJsonToSpice(circuitJson as any)
|
|
6
|
+
const baseSpiceString = spiceNetlist.toSpiceString()
|
|
7
|
+
|
|
8
|
+
const lines = baseSpiceString.split("\n").filter((l) => l.trim() !== "")
|
|
9
|
+
const componentLines = lines.filter(
|
|
10
|
+
(l) => !l.startsWith("*") && !l.startsWith(".") && l.trim() !== "",
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
const allNodes = new Set<string>()
|
|
14
|
+
const capacitorNodes = new Set<string>()
|
|
15
|
+
|
|
16
|
+
for (const line of componentLines) {
|
|
17
|
+
const parts = line.trim().split(/\s+/)
|
|
18
|
+
if (parts.length < 3) continue
|
|
19
|
+
|
|
20
|
+
const componentType = parts[0][0].toUpperCase()
|
|
21
|
+
let nodesOnLine: string[] = []
|
|
22
|
+
|
|
23
|
+
if (["R", "C", "L", "V", "I", "D"].includes(componentType)) {
|
|
24
|
+
nodesOnLine = parts.slice(1, 3)
|
|
25
|
+
} else if (componentType === "Q" && parts.length >= 4) {
|
|
26
|
+
// BJT
|
|
27
|
+
nodesOnLine = parts.slice(1, 4)
|
|
28
|
+
} else if (componentType === "M" && parts.length >= 5) {
|
|
29
|
+
// MOSFET
|
|
30
|
+
nodesOnLine = parts.slice(1, 5)
|
|
31
|
+
} else if (componentType === "X") {
|
|
32
|
+
// Subcircuit
|
|
33
|
+
// Assume last part is model name, everything in between is a node
|
|
34
|
+
nodesOnLine = parts.slice(1, -1)
|
|
35
|
+
} else {
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
nodesOnLine.forEach((node) => allNodes.add(node))
|
|
40
|
+
|
|
41
|
+
if (componentType === "C") {
|
|
42
|
+
nodesOnLine.forEach((node) => capacitorNodes.add(node))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Do not probe/set IC for ground
|
|
47
|
+
allNodes.delete("0")
|
|
48
|
+
capacitorNodes.delete("0")
|
|
49
|
+
|
|
50
|
+
const icLines = Array.from(capacitorNodes).map((node) => `.ic V(${node})=0`)
|
|
51
|
+
|
|
52
|
+
const probeNodes = Array.from(allNodes).map((node) => `V(${node})`)
|
|
53
|
+
const probeLine =
|
|
54
|
+
probeNodes.length > 0 ? `.probe ${probeNodes.join(" ")}` : ""
|
|
55
|
+
|
|
56
|
+
const tranLine = ".tran 0.1ms 50ms UIC"
|
|
57
|
+
|
|
58
|
+
const endStatement = ".end"
|
|
59
|
+
const originalLines = baseSpiceString.split("\n")
|
|
60
|
+
let endIndex = -1
|
|
61
|
+
for (let i = originalLines.length - 1; i >= 0; i--) {
|
|
62
|
+
if (originalLines[i].trim().toLowerCase().startsWith(endStatement)) {
|
|
63
|
+
endIndex = i
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const injectionLines = [...icLines, probeLine, tranLine].filter(Boolean)
|
|
69
|
+
|
|
70
|
+
let finalLines: string[]
|
|
71
|
+
|
|
72
|
+
if (endIndex !== -1) {
|
|
73
|
+
const beforeEnd = originalLines.slice(0, endIndex)
|
|
74
|
+
const endLineAndAfter = originalLines.slice(endIndex)
|
|
75
|
+
finalLines = [...beforeEnd, ...injectionLines, ...endLineAndAfter]
|
|
76
|
+
} else {
|
|
77
|
+
finalLines = [...originalLines, ...injectionLines, endStatement]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return finalLines.join("\n")
|
|
81
|
+
}
|