circuitjson-toolkit 1.0.3 → 1.0.16
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/AGENTS.md +5 -3
- package/README.md +21 -2
- package/docs/api.md +50 -4
- package/docs/model-format.md +21 -3
- package/package.json +3 -2
- package/spec/library-scope.md +4 -1
- package/src/core/CircuitJsonBomBuilder.mjs +143 -0
- package/src/core/CircuitJsonDocument.mjs +46 -13
- package/src/core/CircuitJsonElementValidator.mjs +990 -0
- package/src/core/CircuitJsonIndexer.mjs +773 -4
- package/src/core/CircuitJsonManufacturingBuilder.mjs +898 -0
- package/src/core/CircuitJsonManufacturingDownloadBuilder.mjs +196 -0
- package/src/core/CircuitJsonParser.mjs +22 -6
- package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +329 -0
- package/src/core/CircuitJsonPcbCopperGeometry.mjs +503 -0
- package/src/core/CircuitJsonPcbDrawingStyle.mjs +88 -0
- package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +172 -0
- package/src/core/CircuitJsonPcbNetMetadata.mjs +247 -0
- package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +70 -0
- package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +992 -0
- package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +872 -0
- package/src/core/CircuitJsonPcbPrimitiveFields.mjs +233 -0
- package/src/core/CircuitJsonPcbPrimitiveGeometry.mjs +142 -0
- package/src/core/CircuitJsonPcbPrimitiveGroups.mjs +305 -0
- package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +65 -0
- package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +895 -0
- package/src/core/CircuitJsonPcbTraceLengthModel.mjs +257 -0
- package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +683 -0
- package/src/core/CircuitJsonSourceMetadata.mjs +233 -0
- package/src/core/CircuitJsonSupportMatrixBuilder.mjs +481 -0
- package/src/core/CircuitJsonUnits.mjs +133 -8
- package/src/core/PcbBoundsSelectionModel.mjs +250 -0
- package/src/core/PcbCandidateSelectionModel.mjs +77 -0
- package/src/core/PcbDiagnosticFocusModel.mjs +423 -0
- package/src/core/PcbInteractionPrimitiveModel.mjs +560 -0
- package/src/core/SelectedPartCircuitJsonExportAdapter.mjs +335 -0
- package/src/core/spice/SpiceCompatibilityPreprocessor.mjs +139 -0
- package/src/core/spice/SpiceDirectiveParser.mjs +231 -0
- package/src/core/spice/SpiceFallbackSimulationEngine.mjs +168 -0
- package/src/core/spice/SpiceSimulationDiagnostics.mjs +234 -0
- package/src/core/spice/SpiceSimulationGraphBuilder.mjs +421 -0
- package/src/core/spice/SpiceSimulationGraphSummary.mjs +90 -0
- package/src/core/spice/SpiceSimulationService.mjs +92 -0
- package/src/core/spice/SpiceTimeSeriesNormalizer.mjs +132 -0
- package/src/index.mjs +8 -0
- package/src/renderers.mjs +29 -0
- package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +128 -0
- package/src/ui/CircuitJsonPcbSvgRenderer.mjs +964 -0
- package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +168 -0
- package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +138 -0
- package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +114 -0
- package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +130 -0
- package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +994 -0
- package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +439 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { SpiceDirectiveParser } from './SpiceDirectiveParser.mjs'
|
|
2
|
+
import { SpiceTimeSeriesNormalizer } from './SpiceTimeSeriesNormalizer.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Converts simulator result rows into CircuitJSON transient graph elements.
|
|
6
|
+
*/
|
|
7
|
+
export class SpiceSimulationGraphBuilder {
|
|
8
|
+
/**
|
|
9
|
+
* Builds CircuitJSON transient graph elements from a simulation result.
|
|
10
|
+
* @param {object} result Simulator result.
|
|
11
|
+
* @param {string} spiceString Preprocessed SPICE netlist text.
|
|
12
|
+
* @param {{ simulationExperimentId?: string }} [options] Graph output options.
|
|
13
|
+
* @returns {object[]}
|
|
14
|
+
*/
|
|
15
|
+
static buildCircuitJsonGraphs(result, spiceString, options = {}) {
|
|
16
|
+
const tran = SpiceDirectiveParser.parseTransient(spiceString)
|
|
17
|
+
const graphs = SpiceTimeSeriesNormalizer.resampleGraphs(
|
|
18
|
+
SpiceSimulationGraphBuilder.#buildGraphs(result, spiceString),
|
|
19
|
+
tran
|
|
20
|
+
)
|
|
21
|
+
const simulationExperimentId =
|
|
22
|
+
options.simulationExperimentId || 'simulation_experiment_0'
|
|
23
|
+
|
|
24
|
+
return graphs.map((graph, index) =>
|
|
25
|
+
graph.graphType === 'voltage'
|
|
26
|
+
? SpiceSimulationGraphBuilder.#voltageGraphElement(
|
|
27
|
+
graph,
|
|
28
|
+
index,
|
|
29
|
+
tran,
|
|
30
|
+
simulationExperimentId
|
|
31
|
+
)
|
|
32
|
+
: SpiceSimulationGraphBuilder.#currentGraphElement(
|
|
33
|
+
graph,
|
|
34
|
+
index,
|
|
35
|
+
tran,
|
|
36
|
+
simulationExperimentId
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Builds a complete simulation experiment element set.
|
|
43
|
+
* @param {object} result Simulator result.
|
|
44
|
+
* @param {string} spiceString Preprocessed SPICE netlist text.
|
|
45
|
+
* @param {{ simulationExperimentId?: string, name?: string }} [options] Experiment output options.
|
|
46
|
+
* @returns {object[]}
|
|
47
|
+
*/
|
|
48
|
+
static buildCircuitJsonExperiment(result, spiceString, options = {}) {
|
|
49
|
+
const simulationExperimentId =
|
|
50
|
+
options.simulationExperimentId || 'simulation_experiment_0'
|
|
51
|
+
const graphs = SpiceSimulationGraphBuilder.buildCircuitJsonGraphs(
|
|
52
|
+
result,
|
|
53
|
+
spiceString,
|
|
54
|
+
{ simulationExperimentId }
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
type: 'simulation_experiment',
|
|
60
|
+
simulation_experiment_id: simulationExperimentId,
|
|
61
|
+
name: options.name || 'SPICE transient analysis',
|
|
62
|
+
experiment_type: 'spice_transient_analysis'
|
|
63
|
+
},
|
|
64
|
+
...graphs
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Finds requested transient plot tokens that did not produce graph models.
|
|
70
|
+
* @param {object} result Simulator result.
|
|
71
|
+
* @param {string} spiceString Preprocessed SPICE netlist text.
|
|
72
|
+
* @returns {{ normalizedToken: string, originalToken: string }[]}
|
|
73
|
+
*/
|
|
74
|
+
static findMissingRequestedPlots(result, spiceString) {
|
|
75
|
+
const requestedPlots =
|
|
76
|
+
SpiceDirectiveParser.parseRequestedPlots(spiceString)
|
|
77
|
+
if (!requestedPlots) return []
|
|
78
|
+
|
|
79
|
+
const fulfilledTokens = new Set(
|
|
80
|
+
SpiceSimulationGraphBuilder.#buildGraphs(result, spiceString)
|
|
81
|
+
.map((graph) => graph.normalizedToken)
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return [...requestedPlots]
|
|
86
|
+
.filter(
|
|
87
|
+
([normalizedToken]) => !fulfilledTokens.has(normalizedToken)
|
|
88
|
+
)
|
|
89
|
+
.map(([normalizedToken, originalToken]) => ({
|
|
90
|
+
normalizedToken,
|
|
91
|
+
originalToken
|
|
92
|
+
}))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Builds normalized graph models from simulator data rows.
|
|
97
|
+
* @param {object} result Simulator result.
|
|
98
|
+
* @param {string} spiceString Preprocessed SPICE netlist text.
|
|
99
|
+
* @returns {object[]}
|
|
100
|
+
*/
|
|
101
|
+
static #buildGraphs(result, spiceString) {
|
|
102
|
+
if (!result?.data || result.dataType !== 'real') return []
|
|
103
|
+
|
|
104
|
+
const timeRow = result.data.find((row) => row.type === 'time')
|
|
105
|
+
if (!Array.isArray(timeRow?.values)) return []
|
|
106
|
+
|
|
107
|
+
const timeValues = timeRow.values
|
|
108
|
+
const voltageRows = result.data.filter(
|
|
109
|
+
(row) => row.type === 'voltage' && Array.isArray(row.values)
|
|
110
|
+
)
|
|
111
|
+
const currentRows = result.data.filter(
|
|
112
|
+
(row) => row.type === 'current' && Array.isArray(row.values)
|
|
113
|
+
)
|
|
114
|
+
const voltageData = new Map(
|
|
115
|
+
voltageRows.map((row) => [
|
|
116
|
+
SpiceDirectiveParser.normalizeVector(row.name),
|
|
117
|
+
row.values
|
|
118
|
+
])
|
|
119
|
+
)
|
|
120
|
+
const currentData = new Map(
|
|
121
|
+
currentRows.map((row) => [
|
|
122
|
+
SpiceDirectiveParser.normalizeVector(row.name),
|
|
123
|
+
row.values
|
|
124
|
+
])
|
|
125
|
+
)
|
|
126
|
+
const requestedPlots =
|
|
127
|
+
SpiceDirectiveParser.parseRequestedPlots(spiceString)
|
|
128
|
+
const voltageMetadata =
|
|
129
|
+
SpiceDirectiveParser.extractVoltageProbeMetadata(spiceString)
|
|
130
|
+
const currentMetadata =
|
|
131
|
+
SpiceDirectiveParser.extractCurrentProbeMetadata(spiceString)
|
|
132
|
+
|
|
133
|
+
if (!requestedPlots) {
|
|
134
|
+
return [
|
|
135
|
+
...voltageRows.map((row) =>
|
|
136
|
+
SpiceSimulationGraphBuilder.#voltageGraphFromRow(
|
|
137
|
+
row,
|
|
138
|
+
timeValues,
|
|
139
|
+
voltageMetadata
|
|
140
|
+
)
|
|
141
|
+
),
|
|
142
|
+
...currentRows.map((row) =>
|
|
143
|
+
SpiceSimulationGraphBuilder.#currentGraphFromRow(
|
|
144
|
+
row,
|
|
145
|
+
timeValues,
|
|
146
|
+
currentMetadata
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const graphs = []
|
|
153
|
+
for (const [normalizedToken, originalToken] of requestedPlots) {
|
|
154
|
+
const graph =
|
|
155
|
+
SpiceSimulationGraphBuilder.#voltageGraphFromPlot({
|
|
156
|
+
normalizedToken,
|
|
157
|
+
originalToken,
|
|
158
|
+
timeValues,
|
|
159
|
+
voltageData,
|
|
160
|
+
voltageMetadata
|
|
161
|
+
}) ??
|
|
162
|
+
SpiceSimulationGraphBuilder.#currentGraphFromPlot({
|
|
163
|
+
normalizedToken,
|
|
164
|
+
originalToken,
|
|
165
|
+
timeValues,
|
|
166
|
+
currentData,
|
|
167
|
+
currentMetadata
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
if (graph) graphs.push(graph)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return graphs
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Builds one voltage graph from an available result row.
|
|
178
|
+
* @param {object} row Simulator data row.
|
|
179
|
+
* @param {number[]} timeValues Time values in seconds.
|
|
180
|
+
* @param {Map<string, object>} voltageMetadata Probe metadata by vector.
|
|
181
|
+
* @returns {object}
|
|
182
|
+
*/
|
|
183
|
+
static #voltageGraphFromRow(row, timeValues, voltageMetadata) {
|
|
184
|
+
const normalized = SpiceDirectiveParser.normalizeVector(row.name)
|
|
185
|
+
const metadata = voltageMetadata.get(normalized)
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
graphType: 'voltage',
|
|
189
|
+
name:
|
|
190
|
+
metadata?.name ??
|
|
191
|
+
SpiceSimulationGraphBuilder.#voltageName(row.name),
|
|
192
|
+
time: timeValues,
|
|
193
|
+
values: row.values,
|
|
194
|
+
metadata
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Builds one current graph from an available result row.
|
|
200
|
+
* @param {object} row Simulator data row.
|
|
201
|
+
* @param {number[]} timeValues Time values in seconds.
|
|
202
|
+
* @param {Map<string, object>} currentMetadata Probe metadata by vector.
|
|
203
|
+
* @returns {object}
|
|
204
|
+
*/
|
|
205
|
+
static #currentGraphFromRow(row, timeValues, currentMetadata) {
|
|
206
|
+
const normalized = SpiceDirectiveParser.normalizeVector(row.name)
|
|
207
|
+
const metadata = currentMetadata.get(normalized)
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
graphType: 'current',
|
|
211
|
+
name:
|
|
212
|
+
metadata?.name ??
|
|
213
|
+
SpiceSimulationGraphBuilder.#currentName(row.name),
|
|
214
|
+
time: timeValues,
|
|
215
|
+
values: row.values,
|
|
216
|
+
metadata
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Builds one voltage graph for a requested plot token.
|
|
222
|
+
* @param {object} options Plot options.
|
|
223
|
+
* @returns {object | null}
|
|
224
|
+
*/
|
|
225
|
+
static #voltageGraphFromPlot(options) {
|
|
226
|
+
const {
|
|
227
|
+
normalizedToken,
|
|
228
|
+
originalToken,
|
|
229
|
+
timeValues,
|
|
230
|
+
voltageData,
|
|
231
|
+
voltageMetadata
|
|
232
|
+
} = options
|
|
233
|
+
if (!normalizedToken.startsWith('v(')) return null
|
|
234
|
+
|
|
235
|
+
const diffMatch = originalToken.match(/^v\(([^,]+),\s*([^)]+)\)$/i)
|
|
236
|
+
let values = voltageData.get(normalizedToken)
|
|
237
|
+
|
|
238
|
+
if (!values && diffMatch?.[1] && diffMatch?.[2]) {
|
|
239
|
+
values = SpiceSimulationGraphBuilder.#differentialValues(
|
|
240
|
+
diffMatch[1],
|
|
241
|
+
diffMatch[2],
|
|
242
|
+
voltageData
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!values) return null
|
|
247
|
+
|
|
248
|
+
const metadata = voltageMetadata.get(normalizedToken)
|
|
249
|
+
return {
|
|
250
|
+
graphType: 'voltage',
|
|
251
|
+
normalizedToken,
|
|
252
|
+
name:
|
|
253
|
+
metadata?.name ??
|
|
254
|
+
SpiceSimulationGraphBuilder.#voltageName(originalToken),
|
|
255
|
+
time: timeValues,
|
|
256
|
+
values,
|
|
257
|
+
metadata
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Builds one current graph for a requested plot token.
|
|
263
|
+
* @param {object} options Plot options.
|
|
264
|
+
* @returns {object | null}
|
|
265
|
+
*/
|
|
266
|
+
static #currentGraphFromPlot(options) {
|
|
267
|
+
const {
|
|
268
|
+
normalizedToken,
|
|
269
|
+
originalToken,
|
|
270
|
+
timeValues,
|
|
271
|
+
currentData,
|
|
272
|
+
currentMetadata
|
|
273
|
+
} = options
|
|
274
|
+
if (!normalizedToken.startsWith('i(')) return null
|
|
275
|
+
|
|
276
|
+
const values = currentData.get(normalizedToken)
|
|
277
|
+
if (!values) return null
|
|
278
|
+
|
|
279
|
+
const metadata = currentMetadata.get(normalizedToken)
|
|
280
|
+
return {
|
|
281
|
+
graphType: 'current',
|
|
282
|
+
normalizedToken,
|
|
283
|
+
name:
|
|
284
|
+
metadata?.name ??
|
|
285
|
+
SpiceSimulationGraphBuilder.#currentName(originalToken),
|
|
286
|
+
time: timeValues,
|
|
287
|
+
values,
|
|
288
|
+
metadata
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Resolves differential voltage values from two node vectors.
|
|
294
|
+
* @param {string} positiveNode Positive node name.
|
|
295
|
+
* @param {string} referenceNode Reference node name.
|
|
296
|
+
* @param {Map<string, number[]>} voltageData Voltage data map.
|
|
297
|
+
* @returns {number[] | undefined}
|
|
298
|
+
*/
|
|
299
|
+
static #differentialValues(positiveNode, referenceNode, voltageData) {
|
|
300
|
+
const positiveValues = voltageData.get(
|
|
301
|
+
`v(${String(positiveNode).trim().toLowerCase()})`
|
|
302
|
+
)
|
|
303
|
+
const referenceValues = voltageData.get(
|
|
304
|
+
`v(${String(referenceNode).trim().toLowerCase()})`
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
if (!positiveValues || !referenceValues) return undefined
|
|
308
|
+
|
|
309
|
+
return positiveValues.map((value, index) =>
|
|
310
|
+
SpiceSimulationGraphBuilder.#round(
|
|
311
|
+
value - (referenceValues[index] ?? 0)
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Converts a normalized graph to a voltage graph element.
|
|
318
|
+
* @param {object} graph Graph model.
|
|
319
|
+
* @param {number} index Graph index.
|
|
320
|
+
* @param {object | null} tran Transient timing parameters.
|
|
321
|
+
* @param {string} simulationExperimentId Simulation experiment id.
|
|
322
|
+
* @returns {object}
|
|
323
|
+
*/
|
|
324
|
+
static #voltageGraphElement(graph, index, tran, simulationExperimentId) {
|
|
325
|
+
const graphIdSource =
|
|
326
|
+
graph.metadata?.simulation_voltage_probe_id ??
|
|
327
|
+
`${index}_${graph.name}`
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
type: 'simulation_transient_voltage_graph',
|
|
331
|
+
simulation_experiment_id: simulationExperimentId,
|
|
332
|
+
simulation_transient_voltage_graph_id:
|
|
333
|
+
'simulation_graph_' + graphIdSource,
|
|
334
|
+
name: graph.name,
|
|
335
|
+
voltage_levels: graph.values.map((value) =>
|
|
336
|
+
SpiceSimulationGraphBuilder.#round(value)
|
|
337
|
+
),
|
|
338
|
+
timestamps_ms: graph.time.map((time) =>
|
|
339
|
+
SpiceSimulationGraphBuilder.#round(time * 1000)
|
|
340
|
+
),
|
|
341
|
+
start_time_ms: (tran?.tstart ?? 0) * 1000,
|
|
342
|
+
time_per_step: (tran?.tstep ?? 0) * 1000,
|
|
343
|
+
end_time_ms: (tran?.tstop ?? 0) * 1000,
|
|
344
|
+
source_probe_id: graph.metadata?.simulation_voltage_probe_id,
|
|
345
|
+
source_probe_name: graph.metadata?.name,
|
|
346
|
+
source_node_name: graph.metadata?.source_node_name,
|
|
347
|
+
reference_node_name: graph.metadata?.reference_node_name
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Converts a normalized graph to a current graph element.
|
|
353
|
+
* @param {object} graph Graph model.
|
|
354
|
+
* @param {number} index Graph index.
|
|
355
|
+
* @param {object | null} tran Transient timing parameters.
|
|
356
|
+
* @param {string} simulationExperimentId Simulation experiment id.
|
|
357
|
+
* @returns {object}
|
|
358
|
+
*/
|
|
359
|
+
static #currentGraphElement(graph, index, tran, simulationExperimentId) {
|
|
360
|
+
const graphIdSource =
|
|
361
|
+
graph.metadata?.simulation_current_probe_id ??
|
|
362
|
+
`${index}_${graph.name}`
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
type: 'simulation_transient_current_graph',
|
|
366
|
+
simulation_experiment_id: simulationExperimentId,
|
|
367
|
+
simulation_transient_current_graph_id:
|
|
368
|
+
'simulation_graph_' + graphIdSource,
|
|
369
|
+
name: graph.name,
|
|
370
|
+
current_levels: graph.values.map((value) =>
|
|
371
|
+
SpiceSimulationGraphBuilder.#round(value)
|
|
372
|
+
),
|
|
373
|
+
timestamps_ms: graph.time.map((time) =>
|
|
374
|
+
SpiceSimulationGraphBuilder.#round(time * 1000)
|
|
375
|
+
),
|
|
376
|
+
start_time_ms: (tran?.tstart ?? 0) * 1000,
|
|
377
|
+
time_per_step: (tran?.tstep ?? 0) * 1000,
|
|
378
|
+
end_time_ms: (tran?.tstop ?? 0) * 1000,
|
|
379
|
+
source_probe_id: graph.metadata?.simulation_current_probe_id,
|
|
380
|
+
source_probe_name: graph.metadata?.name,
|
|
381
|
+
source_component_id: graph.metadata?.source_component_id,
|
|
382
|
+
source_trace_id: graph.metadata?.source_trace_id
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Returns a display name for a voltage vector token.
|
|
388
|
+
* @param {string} rawName Raw vector token.
|
|
389
|
+
* @returns {string}
|
|
390
|
+
*/
|
|
391
|
+
static #voltageName(rawName) {
|
|
392
|
+
const diffMatch = String(rawName || '').match(
|
|
393
|
+
/^v\(([^,]+),\s*([^)]+)\)$/i
|
|
394
|
+
)
|
|
395
|
+
if (diffMatch?.[1] && diffMatch?.[2]) {
|
|
396
|
+
return `${diffMatch[1].trim()}-${diffMatch[2].trim()}`
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const match = String(rawName || '').match(/^v\((.*)\)$/i)
|
|
400
|
+
return match?.[1] ?? String(rawName || '')
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Returns a display name for a current vector token.
|
|
405
|
+
* @param {string} rawName Raw vector token.
|
|
406
|
+
* @returns {string}
|
|
407
|
+
*/
|
|
408
|
+
static #currentName(rawName) {
|
|
409
|
+
const match = String(rawName || '').match(/^i\((.*)\)$/i)
|
|
410
|
+
return match?.[1] ?? String(rawName || '')
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Rounds numeric output to a stable precision.
|
|
415
|
+
* @param {number} value Numeric value.
|
|
416
|
+
* @returns {number}
|
|
417
|
+
*/
|
|
418
|
+
static #round(value) {
|
|
419
|
+
return Number(Number(value).toPrecision(12))
|
|
420
|
+
}
|
|
421
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds deterministic summaries for transient graph elements.
|
|
3
|
+
*/
|
|
4
|
+
export class SpiceSimulationGraphSummary {
|
|
5
|
+
/**
|
|
6
|
+
* Summarizes transient graph elements for renderer and test callers.
|
|
7
|
+
* @param {object[]} simulationResultCircuitJson Graph-only CircuitJSON elements.
|
|
8
|
+
* @returns {object}
|
|
9
|
+
*/
|
|
10
|
+
static summarize(simulationResultCircuitJson) {
|
|
11
|
+
const graphs = Array.isArray(simulationResultCircuitJson)
|
|
12
|
+
? simulationResultCircuitJson.filter((element) =>
|
|
13
|
+
SpiceSimulationGraphSummary.#isTransientGraph(element)
|
|
14
|
+
)
|
|
15
|
+
: []
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
graphCount: graphs.length,
|
|
19
|
+
voltageGraphCount: graphs.filter(
|
|
20
|
+
(element) =>
|
|
21
|
+
element.type === 'simulation_transient_voltage_graph'
|
|
22
|
+
).length,
|
|
23
|
+
currentGraphCount: graphs.filter(
|
|
24
|
+
(element) =>
|
|
25
|
+
element.type === 'simulation_transient_current_graph'
|
|
26
|
+
).length,
|
|
27
|
+
graphs: graphs.map((element) =>
|
|
28
|
+
SpiceSimulationGraphSummary.#summarizeGraph(element)
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns true when an element is a transient graph.
|
|
35
|
+
* @param {object} element Candidate CircuitJSON element.
|
|
36
|
+
* @returns {boolean}
|
|
37
|
+
*/
|
|
38
|
+
static #isTransientGraph(element) {
|
|
39
|
+
return (
|
|
40
|
+
element?.type === 'simulation_transient_voltage_graph' ||
|
|
41
|
+
element?.type === 'simulation_transient_current_graph'
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Summarizes one transient graph element.
|
|
47
|
+
* @param {object} element Transient graph element.
|
|
48
|
+
* @returns {object}
|
|
49
|
+
*/
|
|
50
|
+
static #summarizeGraph(element) {
|
|
51
|
+
const isVoltage = element.type === 'simulation_transient_voltage_graph'
|
|
52
|
+
const values = isVoltage
|
|
53
|
+
? element.voltage_levels
|
|
54
|
+
: element.current_levels
|
|
55
|
+
const finiteValues = Array.isArray(values)
|
|
56
|
+
? values.filter(Number.isFinite)
|
|
57
|
+
: []
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
id: isVoltage
|
|
61
|
+
? element.simulation_transient_voltage_graph_id
|
|
62
|
+
: element.simulation_transient_current_graph_id,
|
|
63
|
+
graphType: isVoltage ? 'voltage' : 'current',
|
|
64
|
+
name: element.name,
|
|
65
|
+
pointCount: Array.isArray(values) ? values.length : 0,
|
|
66
|
+
startTimeMs: element.start_time_ms,
|
|
67
|
+
endTimeMs: element.end_time_ms,
|
|
68
|
+
timePerStepMs: element.time_per_step,
|
|
69
|
+
min: SpiceSimulationGraphSummary.#round(
|
|
70
|
+
finiteValues.length ? Math.min(...finiteValues) : undefined
|
|
71
|
+
),
|
|
72
|
+
max: SpiceSimulationGraphSummary.#round(
|
|
73
|
+
finiteValues.length ? Math.max(...finiteValues) : undefined
|
|
74
|
+
),
|
|
75
|
+
firstValue: SpiceSimulationGraphSummary.#round(values?.[0]),
|
|
76
|
+
lastValue: SpiceSimulationGraphSummary.#round(values?.at(-1))
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Rounds finite numeric output to a stable precision.
|
|
82
|
+
* @param {unknown} value Numeric value.
|
|
83
|
+
* @returns {number | undefined}
|
|
84
|
+
*/
|
|
85
|
+
static #round(value) {
|
|
86
|
+
return Number.isFinite(value)
|
|
87
|
+
? Number(Number(value).toPrecision(12))
|
|
88
|
+
: undefined
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { CircuitJsonDocument } from '../CircuitJsonDocument.mjs'
|
|
2
|
+
import { SpiceCompatibilityPreprocessor } from './SpiceCompatibilityPreprocessor.mjs'
|
|
3
|
+
import { SpiceFallbackSimulationEngine } from './SpiceFallbackSimulationEngine.mjs'
|
|
4
|
+
import { SpiceSimulationDiagnostics } from './SpiceSimulationDiagnostics.mjs'
|
|
5
|
+
import { SpiceSimulationGraphBuilder } from './SpiceSimulationGraphBuilder.mjs'
|
|
6
|
+
import { SpiceSimulationGraphSummary } from './SpiceSimulationGraphSummary.mjs'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Runs SPICE transient simulations through an injectable engine boundary.
|
|
10
|
+
*/
|
|
11
|
+
export class SpiceSimulationService {
|
|
12
|
+
/** @type {{ simulate: (spiceString: string) => Promise<object> | object }} */
|
|
13
|
+
#engine
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {{ engine?: { simulate: (spiceString: string) => Promise<object> | object } }} [dependencies] Service dependencies.
|
|
17
|
+
*/
|
|
18
|
+
constructor(dependencies = {}) {
|
|
19
|
+
this.#engine =
|
|
20
|
+
dependencies.engine || new SpiceFallbackSimulationEngine()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Simulates a netlist with the default local fallback engine.
|
|
25
|
+
* @param {string} spiceString SPICE netlist text.
|
|
26
|
+
* @returns {Promise<{ simulationResultCircuitJson: object[], simulationCircuitJson: object[], graphSummary: object, diagnostics: object[] }>}
|
|
27
|
+
*/
|
|
28
|
+
static async simulate(spiceString) {
|
|
29
|
+
return new SpiceSimulationService().simulate(spiceString)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Simulates a netlist and returns CircuitJSON transient graph elements.
|
|
34
|
+
* @param {string} spiceString SPICE netlist text.
|
|
35
|
+
* @returns {Promise<{ simulationResultCircuitJson: object[], simulationCircuitJson: object[], graphSummary: object, diagnostics: object[] }>}
|
|
36
|
+
*/
|
|
37
|
+
async simulate(spiceString) {
|
|
38
|
+
const preprocessedNetlist =
|
|
39
|
+
SpiceCompatibilityPreprocessor.rewrite(spiceString)
|
|
40
|
+
const diagnostics = SpiceSimulationDiagnostics.analyze(spiceString)
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const rawResult = await this.#engine.simulate(preprocessedNetlist)
|
|
44
|
+
diagnostics.push(
|
|
45
|
+
...SpiceSimulationDiagnostics.requestedPlotDiagnostics(
|
|
46
|
+
SpiceSimulationGraphBuilder.findMissingRequestedPlots(
|
|
47
|
+
rawResult,
|
|
48
|
+
preprocessedNetlist
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
const simulationCircuitJson =
|
|
53
|
+
SpiceSimulationGraphBuilder.buildCircuitJsonExperiment(
|
|
54
|
+
rawResult,
|
|
55
|
+
preprocessedNetlist
|
|
56
|
+
)
|
|
57
|
+
const simulationResultCircuitJson = simulationCircuitJson.filter(
|
|
58
|
+
(element) =>
|
|
59
|
+
element.type === 'simulation_transient_voltage_graph' ||
|
|
60
|
+
element.type === 'simulation_transient_current_graph'
|
|
61
|
+
)
|
|
62
|
+
const graphSummary = SpiceSimulationGraphSummary.summarize(
|
|
63
|
+
simulationResultCircuitJson
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
CircuitJsonDocument.assertModel(simulationResultCircuitJson)
|
|
67
|
+
CircuitJsonDocument.assertModel(simulationCircuitJson)
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
simulationResultCircuitJson,
|
|
71
|
+
simulationCircuitJson,
|
|
72
|
+
graphSummary,
|
|
73
|
+
diagnostics
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
diagnostics.push({
|
|
77
|
+
severity: 'error',
|
|
78
|
+
message:
|
|
79
|
+
error instanceof Error
|
|
80
|
+
? error.message
|
|
81
|
+
: 'SPICE simulation failed.'
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
simulationResultCircuitJson: [],
|
|
86
|
+
simulationCircuitJson: [],
|
|
87
|
+
graphSummary: SpiceSimulationGraphSummary.summarize([]),
|
|
88
|
+
diagnostics
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|