circuitjson-toolkit 1.0.3 → 1.0.10
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 +21 -2
- package/docs/api.md +50 -4
- package/docs/model-format.md +6 -3
- package/package.json +1 -1
- package/spec/library-scope.md +4 -1
- package/src/core/CircuitJsonBomBuilder.mjs +146 -0
- package/src/core/CircuitJsonDocument.mjs +46 -13
- package/src/core/CircuitJsonElementValidator.mjs +773 -0
- package/src/core/CircuitJsonIndexer.mjs +268 -4
- package/src/core/CircuitJsonManufacturingBuilder.mjs +426 -0
- package/src/core/CircuitJsonParser.mjs +22 -6
- package/src/core/CircuitJsonSupportMatrixBuilder.mjs +259 -0
- package/src/core/CircuitJsonUnits.mjs +133 -8
- 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 +6 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
const NUMBER_SUFFIX_MULTIPLIERS = {
|
|
2
|
+
t: 1e12,
|
|
3
|
+
g: 1e9,
|
|
4
|
+
meg: 1e6,
|
|
5
|
+
k: 1e3,
|
|
6
|
+
m: 1e-3,
|
|
7
|
+
ms: 1e-3,
|
|
8
|
+
u: 1e-6,
|
|
9
|
+
us: 1e-6,
|
|
10
|
+
n: 1e-9,
|
|
11
|
+
ns: 1e-9,
|
|
12
|
+
p: 1e-12,
|
|
13
|
+
ps: 1e-12,
|
|
14
|
+
f: 1e-15,
|
|
15
|
+
fs: 1e-15,
|
|
16
|
+
s: 1
|
|
17
|
+
}
|
|
18
|
+
const VOLTAGE_PROBE_COMMENT_PATTERN =
|
|
19
|
+
/^\s*\*\s*(?:ecadforge_voltage_probe|circuitjson_voltage_probe|simulation_voltage_probe)\s+(.+)\s*$/
|
|
20
|
+
const CURRENT_PROBE_COMMENT_PATTERN =
|
|
21
|
+
/^\s*\*\s*(?:ecadforge_current_probe|circuitjson_current_probe|simulation_current_probe)\s+(.+)\s*$/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parses SPICE directives and metadata comments used by simulation helpers.
|
|
25
|
+
*/
|
|
26
|
+
export class SpiceDirectiveParser {
|
|
27
|
+
/**
|
|
28
|
+
* Parses transient directive timing parameters.
|
|
29
|
+
* @param {string} spiceString SPICE netlist text.
|
|
30
|
+
* @returns {{ tstep?: number, tstop?: number, tstart?: number, tmax?: number, uic?: boolean } | null}
|
|
31
|
+
*/
|
|
32
|
+
static parseTransient(spiceString) {
|
|
33
|
+
for (const rawLine of String(spiceString || '').split(/\r?\n/)) {
|
|
34
|
+
const line = rawLine.trim()
|
|
35
|
+
if (!line || line.startsWith('*')) continue
|
|
36
|
+
if (!line.toLowerCase().startsWith('.tran')) continue
|
|
37
|
+
|
|
38
|
+
const [withoutComment = ''] = line.split(';')
|
|
39
|
+
const tokens = withoutComment.split(/\s+/).filter(Boolean)
|
|
40
|
+
const values = []
|
|
41
|
+
let uic = false
|
|
42
|
+
|
|
43
|
+
for (const token of tokens.slice(1)) {
|
|
44
|
+
if (token.toLowerCase() === 'uic') {
|
|
45
|
+
uic = true
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const value = SpiceDirectiveParser.parseNumber(token)
|
|
50
|
+
if (value !== undefined) values.push(value)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...(values[0] !== undefined ? { tstep: values[0] } : {}),
|
|
55
|
+
...(values[1] !== undefined ? { tstop: values[1] } : {}),
|
|
56
|
+
...(values[2] !== undefined ? { tstart: values[2] } : {}),
|
|
57
|
+
...(values[3] !== undefined ? { tmax: values[3] } : {}),
|
|
58
|
+
...(uic ? { uic: true } : {})
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parses requested transient plot tokens from the first PRINT directive.
|
|
67
|
+
* @param {string} spiceString SPICE netlist text.
|
|
68
|
+
* @returns {Map<string, string> | null}
|
|
69
|
+
*/
|
|
70
|
+
static parseRequestedPlots(spiceString) {
|
|
71
|
+
const match = String(spiceString || '').match(/\.print\s+tran\s+(.*)/i)
|
|
72
|
+
if (!match?.[1]) return null
|
|
73
|
+
|
|
74
|
+
const tokens = match[1].match(/[VI]\s*\([^)]+\)/gi)
|
|
75
|
+
if (!tokens) return null
|
|
76
|
+
|
|
77
|
+
const plots = new Map()
|
|
78
|
+
for (const token of tokens) {
|
|
79
|
+
const normalizedToken = SpiceDirectiveParser.normalizeVector(token)
|
|
80
|
+
if (!plots.has(normalizedToken)) {
|
|
81
|
+
plots.set(normalizedToken, token)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return plots
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Extracts voltage probe metadata comments from a netlist.
|
|
90
|
+
* @param {string} spiceString SPICE netlist text.
|
|
91
|
+
* @returns {Map<string, object>}
|
|
92
|
+
*/
|
|
93
|
+
static extractVoltageProbeMetadata(spiceString) {
|
|
94
|
+
return SpiceDirectiveParser.#extractProbeMetadata(
|
|
95
|
+
spiceString,
|
|
96
|
+
VOLTAGE_PROBE_COMMENT_PATTERN,
|
|
97
|
+
(parsed) => {
|
|
98
|
+
if (
|
|
99
|
+
typeof parsed.simulation_voltage_probe_id !== 'string' ||
|
|
100
|
+
typeof parsed.spice_vector !== 'string' ||
|
|
101
|
+
typeof parsed.source_node_name !== 'string'
|
|
102
|
+
) {
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
simulation_voltage_probe_id:
|
|
108
|
+
parsed.simulation_voltage_probe_id,
|
|
109
|
+
name:
|
|
110
|
+
typeof parsed.name === 'string'
|
|
111
|
+
? parsed.name
|
|
112
|
+
: undefined,
|
|
113
|
+
spice_vector: parsed.spice_vector,
|
|
114
|
+
source_node_name: parsed.source_node_name,
|
|
115
|
+
reference_node_name:
|
|
116
|
+
typeof parsed.reference_node_name === 'string'
|
|
117
|
+
? parsed.reference_node_name
|
|
118
|
+
: undefined
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Extracts current probe metadata comments from a netlist.
|
|
126
|
+
* @param {string} spiceString SPICE netlist text.
|
|
127
|
+
* @returns {Map<string, object>}
|
|
128
|
+
*/
|
|
129
|
+
static extractCurrentProbeMetadata(spiceString) {
|
|
130
|
+
return SpiceDirectiveParser.#extractProbeMetadata(
|
|
131
|
+
spiceString,
|
|
132
|
+
CURRENT_PROBE_COMMENT_PATTERN,
|
|
133
|
+
(parsed) => {
|
|
134
|
+
if (
|
|
135
|
+
typeof parsed.simulation_current_probe_id !== 'string' ||
|
|
136
|
+
typeof parsed.spice_vector !== 'string'
|
|
137
|
+
) {
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
simulation_current_probe_id:
|
|
143
|
+
parsed.simulation_current_probe_id,
|
|
144
|
+
name:
|
|
145
|
+
typeof parsed.name === 'string'
|
|
146
|
+
? parsed.name
|
|
147
|
+
: undefined,
|
|
148
|
+
spice_vector: parsed.spice_vector,
|
|
149
|
+
source_component_id:
|
|
150
|
+
typeof parsed.source_component_id === 'string'
|
|
151
|
+
? parsed.source_component_id
|
|
152
|
+
: undefined,
|
|
153
|
+
source_trace_id:
|
|
154
|
+
typeof parsed.source_trace_id === 'string'
|
|
155
|
+
? parsed.source_trace_id
|
|
156
|
+
: undefined
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Normalizes a simulator vector token for map lookups.
|
|
164
|
+
* @param {string} value Raw vector token.
|
|
165
|
+
* @returns {string}
|
|
166
|
+
*/
|
|
167
|
+
static normalizeVector(value) {
|
|
168
|
+
return String(value || '')
|
|
169
|
+
.toLowerCase()
|
|
170
|
+
.replace(/\s/g, '')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Parses a SPICE numeric token with common suffix multipliers.
|
|
175
|
+
* @param {string} token Numeric token.
|
|
176
|
+
* @returns {number | undefined}
|
|
177
|
+
*/
|
|
178
|
+
static parseNumber(token) {
|
|
179
|
+
const normalized = String(token || '')
|
|
180
|
+
.replace(/[,]/g, '')
|
|
181
|
+
.toLowerCase()
|
|
182
|
+
const match = normalized.match(
|
|
183
|
+
/^([+-]?\d*\.?\d+(?:e[+-]?\d+)?)([a-z]+)?$/i
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if (!match) return undefined
|
|
187
|
+
|
|
188
|
+
const base = Number.parseFloat(match[1] || '')
|
|
189
|
+
if (!Number.isFinite(base)) return undefined
|
|
190
|
+
|
|
191
|
+
const suffix = match[2] || ''
|
|
192
|
+
if (!suffix) return base
|
|
193
|
+
|
|
194
|
+
const multiplier =
|
|
195
|
+
NUMBER_SUFFIX_MULTIPLIERS[suffix] ??
|
|
196
|
+
NUMBER_SUFFIX_MULTIPLIERS[suffix.replace(/s$/, '')] ??
|
|
197
|
+
1
|
|
198
|
+
|
|
199
|
+
return base * multiplier
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Extracts JSON probe metadata comments with a validator callback.
|
|
204
|
+
* @param {string} spiceString SPICE netlist text.
|
|
205
|
+
* @param {RegExp} pattern Comment matcher.
|
|
206
|
+
* @param {(parsed: object) => object | null} shapeMetadata Metadata shaper.
|
|
207
|
+
* @returns {Map<string, object>}
|
|
208
|
+
*/
|
|
209
|
+
static #extractProbeMetadata(spiceString, pattern, shapeMetadata) {
|
|
210
|
+
const metadata = new Map()
|
|
211
|
+
|
|
212
|
+
for (const line of String(spiceString || '').split(/\r?\n/)) {
|
|
213
|
+
const match = line.match(pattern)
|
|
214
|
+
if (!match?.[1]) continue
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const shapedMetadata = shapeMetadata(JSON.parse(match[1]))
|
|
218
|
+
if (!shapedMetadata) continue
|
|
219
|
+
|
|
220
|
+
metadata.set(
|
|
221
|
+
SpiceDirectiveParser.normalizeVector(
|
|
222
|
+
shapedMetadata.spice_vector
|
|
223
|
+
),
|
|
224
|
+
shapedMetadata
|
|
225
|
+
)
|
|
226
|
+
} catch {}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return metadata
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { SpiceDirectiveParser } from './SpiceDirectiveParser.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Provides a deterministic local fallback for simple transient examples.
|
|
5
|
+
*/
|
|
6
|
+
export class SpiceFallbackSimulationEngine {
|
|
7
|
+
/**
|
|
8
|
+
* Simulates constant independent sources from a SPICE netlist.
|
|
9
|
+
* @param {string} spiceString SPICE netlist text.
|
|
10
|
+
* @returns {Promise<object>}
|
|
11
|
+
*/
|
|
12
|
+
async simulate(spiceString) {
|
|
13
|
+
const timeValues =
|
|
14
|
+
SpiceFallbackSimulationEngine.#buildTimeValues(spiceString)
|
|
15
|
+
const voltageRows =
|
|
16
|
+
SpiceFallbackSimulationEngine.#buildVoltageRows(spiceString)
|
|
17
|
+
const currentRows =
|
|
18
|
+
SpiceFallbackSimulationEngine.#buildCurrentRows(spiceString)
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
dataType: 'real',
|
|
22
|
+
data: [
|
|
23
|
+
{ name: 'time', type: 'time', values: timeValues },
|
|
24
|
+
...voltageRows.map((row) => ({
|
|
25
|
+
name: row.name,
|
|
26
|
+
type: 'voltage',
|
|
27
|
+
values: timeValues.map(() => row.value)
|
|
28
|
+
})),
|
|
29
|
+
...currentRows.map((row) => ({
|
|
30
|
+
name: row.name,
|
|
31
|
+
type: 'current',
|
|
32
|
+
values: timeValues.map(() => row.value)
|
|
33
|
+
}))
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Builds transient timestamps from the netlist TRAN directive.
|
|
40
|
+
* @param {string} spiceString SPICE netlist text.
|
|
41
|
+
* @returns {number[]}
|
|
42
|
+
*/
|
|
43
|
+
static #buildTimeValues(spiceString) {
|
|
44
|
+
const tran = SpiceDirectiveParser.parseTransient(spiceString)
|
|
45
|
+
const tstep = tran?.tstep && tran.tstep > 0 ? tran.tstep : 0.001
|
|
46
|
+
const tstart = tran?.tstart ?? 0
|
|
47
|
+
const tstop = tran?.tstop && tran.tstop >= tstart ? tran.tstop : tstart
|
|
48
|
+
const stepCount = Math.max(0, Math.round((tstop - tstart) / tstep))
|
|
49
|
+
|
|
50
|
+
return Array.from({ length: stepCount + 1 }, (_, index) =>
|
|
51
|
+
SpiceFallbackSimulationEngine.#round(tstart + index * tstep)
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Builds constant voltage result rows from independent DC voltage sources.
|
|
57
|
+
* @param {string} spiceString SPICE netlist text.
|
|
58
|
+
* @returns {{ name: string, value: number }[]}
|
|
59
|
+
*/
|
|
60
|
+
static #buildVoltageRows(spiceString) {
|
|
61
|
+
const nodeVoltages = new Map([['0', 0]])
|
|
62
|
+
|
|
63
|
+
for (const line of SpiceFallbackSimulationEngine.#componentLines(
|
|
64
|
+
spiceString
|
|
65
|
+
)) {
|
|
66
|
+
const tokens = line.split(/\s+/).filter(Boolean)
|
|
67
|
+
const name = tokens[0] || ''
|
|
68
|
+
if (!/^v/i.test(name) || tokens.length < 4) continue
|
|
69
|
+
|
|
70
|
+
const positiveNode = tokens[1]
|
|
71
|
+
const negativeNode = tokens[2]
|
|
72
|
+
const value =
|
|
73
|
+
SpiceFallbackSimulationEngine.#parseDcValue(tokens.slice(3)) ??
|
|
74
|
+
0
|
|
75
|
+
const negativeValue =
|
|
76
|
+
SpiceFallbackSimulationEngine.#lookupNodeValue(
|
|
77
|
+
nodeVoltages,
|
|
78
|
+
negativeNode
|
|
79
|
+
) ?? 0
|
|
80
|
+
|
|
81
|
+
nodeVoltages.set(
|
|
82
|
+
String(positiveNode).toLowerCase(),
|
|
83
|
+
negativeValue + value
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return [...nodeVoltages.entries()]
|
|
88
|
+
.filter(([node]) => node !== '0')
|
|
89
|
+
.map(([node, value]) => ({
|
|
90
|
+
name: `v(${node})`,
|
|
91
|
+
value: SpiceFallbackSimulationEngine.#round(value)
|
|
92
|
+
}))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Builds constant current result rows from independent DC current sources.
|
|
97
|
+
* @param {string} spiceString SPICE netlist text.
|
|
98
|
+
* @returns {{ name: string, value: number }[]}
|
|
99
|
+
*/
|
|
100
|
+
static #buildCurrentRows(spiceString) {
|
|
101
|
+
const rows = []
|
|
102
|
+
|
|
103
|
+
for (const line of SpiceFallbackSimulationEngine.#componentLines(
|
|
104
|
+
spiceString
|
|
105
|
+
)) {
|
|
106
|
+
const tokens = line.split(/\s+/).filter(Boolean)
|
|
107
|
+
const name = tokens[0] || ''
|
|
108
|
+
if (!/^i/i.test(name) || tokens.length < 4) continue
|
|
109
|
+
|
|
110
|
+
rows.push({
|
|
111
|
+
name: `i(${name})`,
|
|
112
|
+
value:
|
|
113
|
+
SpiceFallbackSimulationEngine.#parseDcValue(
|
|
114
|
+
tokens.slice(3)
|
|
115
|
+
) ?? 0
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return rows
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Returns non-comment component lines from a netlist.
|
|
124
|
+
* @param {string} spiceString SPICE netlist text.
|
|
125
|
+
* @returns {string[]}
|
|
126
|
+
*/
|
|
127
|
+
static #componentLines(spiceString) {
|
|
128
|
+
return String(spiceString || '')
|
|
129
|
+
.split(/\r?\n/)
|
|
130
|
+
.map((line) => line.trim())
|
|
131
|
+
.filter(
|
|
132
|
+
(line) => line && !line.startsWith('*') && !line.startsWith('.')
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Parses a DC source value from component tail tokens.
|
|
138
|
+
* @param {string[]} tokens Component tokens after node names.
|
|
139
|
+
* @returns {number | undefined}
|
|
140
|
+
*/
|
|
141
|
+
static #parseDcValue(tokens) {
|
|
142
|
+
const dcIndex = tokens.findIndex(
|
|
143
|
+
(token) => token.toLowerCase() === 'dc'
|
|
144
|
+
)
|
|
145
|
+
const valueToken = dcIndex >= 0 ? tokens[dcIndex + 1] : tokens[0]
|
|
146
|
+
|
|
147
|
+
return SpiceDirectiveParser.parseNumber(valueToken)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Looks up a node voltage by normalized node name.
|
|
152
|
+
* @param {Map<string, number>} nodeVoltages Node voltage map.
|
|
153
|
+
* @param {string} nodeName Node name.
|
|
154
|
+
* @returns {number | undefined}
|
|
155
|
+
*/
|
|
156
|
+
static #lookupNodeValue(nodeVoltages, nodeName) {
|
|
157
|
+
return nodeVoltages.get(String(nodeName || '').toLowerCase())
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Rounds numeric output to a stable precision.
|
|
162
|
+
* @param {number} value Numeric value.
|
|
163
|
+
* @returns {number}
|
|
164
|
+
*/
|
|
165
|
+
static #round(value) {
|
|
166
|
+
return Number(Number(value).toPrecision(12))
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
const PROBE_METADATA_COMMENT_PATTERNS = [
|
|
2
|
+
{
|
|
3
|
+
probeType: 'voltage',
|
|
4
|
+
pattern:
|
|
5
|
+
/^\s*\*\s*(?:ecadforge_voltage_probe|circuitjson_voltage_probe|simulation_voltage_probe)\s+(.+)\s*$/,
|
|
6
|
+
requiredFields: [
|
|
7
|
+
'simulation_voltage_probe_id',
|
|
8
|
+
'spice_vector',
|
|
9
|
+
'source_node_name'
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
probeType: 'current',
|
|
14
|
+
pattern:
|
|
15
|
+
/^\s*\*\s*(?:ecadforge_current_probe|circuitjson_current_probe|simulation_current_probe)\s+(.+)\s*$/,
|
|
16
|
+
requiredFields: ['simulation_current_probe_id', 'spice_vector']
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Builds diagnostics for SPICE syntax that local helpers cannot resolve.
|
|
22
|
+
*/
|
|
23
|
+
export class SpiceSimulationDiagnostics {
|
|
24
|
+
/**
|
|
25
|
+
* Returns syntax diagnostics for a netlist.
|
|
26
|
+
* @param {string} spiceString SPICE netlist text.
|
|
27
|
+
* @returns {object[]}
|
|
28
|
+
*/
|
|
29
|
+
static analyze(spiceString) {
|
|
30
|
+
const diagnostics = []
|
|
31
|
+
const lines = String(spiceString || '').split(/\r?\n/)
|
|
32
|
+
|
|
33
|
+
lines.forEach((line, index) => {
|
|
34
|
+
const lineNumber = index + 1
|
|
35
|
+
diagnostics.push(
|
|
36
|
+
...SpiceSimulationDiagnostics.#probeMetadataDiagnostics(
|
|
37
|
+
line,
|
|
38
|
+
lineNumber
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if (/^\s*\.lib\b/i.test(line)) {
|
|
43
|
+
diagnostics.push(
|
|
44
|
+
SpiceSimulationDiagnostics.#diagnostic({
|
|
45
|
+
code: 'spice_external_library_unsupported',
|
|
46
|
+
lineNumber,
|
|
47
|
+
message:
|
|
48
|
+
'External .lib directives are not resolved by the local simulator fallback.'
|
|
49
|
+
})
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (/^\s*\.include\b/i.test(line)) {
|
|
54
|
+
diagnostics.push(
|
|
55
|
+
SpiceSimulationDiagnostics.#diagnostic({
|
|
56
|
+
code: 'spice_external_include_unsupported',
|
|
57
|
+
lineNumber,
|
|
58
|
+
message:
|
|
59
|
+
'External .include directives are not resolved by the local simulator fallback.'
|
|
60
|
+
})
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (/\bpwl\s+repeat\b/i.test(line)) {
|
|
65
|
+
diagnostics.push(
|
|
66
|
+
SpiceSimulationDiagnostics.#diagnostic({
|
|
67
|
+
code: 'spice_pwl_repeat_unsupported',
|
|
68
|
+
lineNumber,
|
|
69
|
+
message:
|
|
70
|
+
'PWL REPEAT source syntax is reported for callers that need a full simulator.'
|
|
71
|
+
})
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (/^\s*\.model\b.*\bako\s*:/i.test(line)) {
|
|
76
|
+
diagnostics.push(
|
|
77
|
+
SpiceSimulationDiagnostics.#diagnostic({
|
|
78
|
+
code: 'spice_pspice_ako_model_unsupported',
|
|
79
|
+
lineNumber,
|
|
80
|
+
message:
|
|
81
|
+
'PSPICE AKO model aliases are not resolved by the local simulator fallback.'
|
|
82
|
+
})
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (/^\s*d\S*\s+\S+\s+\S+\s+\S+\s+[+-]?\d/i.test(line)) {
|
|
87
|
+
diagnostics.push(
|
|
88
|
+
SpiceSimulationDiagnostics.#diagnostic({
|
|
89
|
+
code: 'spice_pspice_diode_area_factor_unsupported',
|
|
90
|
+
lineNumber,
|
|
91
|
+
message:
|
|
92
|
+
'PSPICE diode area factor syntax is not evaluated by the local simulator fallback.'
|
|
93
|
+
})
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (/\btable\s*\{/i.test(line)) {
|
|
98
|
+
diagnostics.push(
|
|
99
|
+
SpiceSimulationDiagnostics.#diagnostic({
|
|
100
|
+
code: 'spice_pspice_table_source_unsupported',
|
|
101
|
+
lineNumber,
|
|
102
|
+
message:
|
|
103
|
+
'PSPICE TABLE source syntax is not evaluated by the local simulator fallback.'
|
|
104
|
+
})
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (/^\s*\.model\b.*\bvswitch\s*\(/i.test(line)) {
|
|
109
|
+
diagnostics.push(
|
|
110
|
+
SpiceSimulationDiagnostics.#diagnostic({
|
|
111
|
+
code: 'spice_pspice_vswitch_model_unsupported',
|
|
112
|
+
lineNumber,
|
|
113
|
+
message:
|
|
114
|
+
'PSPICE VSWITCH model syntax is not evaluated by the local simulator fallback.'
|
|
115
|
+
})
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
return diagnostics
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Returns diagnostics for requested plots that did not produce graphs.
|
|
125
|
+
* @param {{ normalizedToken: string, originalToken: string }[]} missingPlots Missing requested plot tokens.
|
|
126
|
+
* @returns {object[]}
|
|
127
|
+
*/
|
|
128
|
+
static requestedPlotDiagnostics(missingPlots) {
|
|
129
|
+
return missingPlots.map((plot) => ({
|
|
130
|
+
severity: 'warning',
|
|
131
|
+
code: 'spice_requested_plot_missing',
|
|
132
|
+
plot: plot.originalToken,
|
|
133
|
+
normalizedPlot: plot.normalizedToken,
|
|
134
|
+
message:
|
|
135
|
+
'Requested transient plot did not match simulator output: ' +
|
|
136
|
+
plot.originalToken
|
|
137
|
+
}))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Returns diagnostics for malformed probe metadata comments on one line.
|
|
142
|
+
* @param {string} line SPICE netlist line.
|
|
143
|
+
* @param {number} lineNumber One-based source line number.
|
|
144
|
+
* @returns {object[]}
|
|
145
|
+
*/
|
|
146
|
+
static #probeMetadataDiagnostics(line, lineNumber) {
|
|
147
|
+
const diagnostics = []
|
|
148
|
+
|
|
149
|
+
for (const definition of PROBE_METADATA_COMMENT_PATTERNS) {
|
|
150
|
+
const match = line.match(definition.pattern)
|
|
151
|
+
if (!match?.[1]) continue
|
|
152
|
+
|
|
153
|
+
let parsed
|
|
154
|
+
try {
|
|
155
|
+
parsed = JSON.parse(match[1])
|
|
156
|
+
} catch {
|
|
157
|
+
diagnostics.push(
|
|
158
|
+
SpiceSimulationDiagnostics.#diagnostic({
|
|
159
|
+
code: 'spice_probe_metadata_invalid_json',
|
|
160
|
+
lineNumber,
|
|
161
|
+
probeType: definition.probeType,
|
|
162
|
+
message:
|
|
163
|
+
SpiceSimulationDiagnostics.#probeTypeLabel(
|
|
164
|
+
definition.probeType
|
|
165
|
+
) + ' probe metadata comment contains invalid JSON.'
|
|
166
|
+
})
|
|
167
|
+
)
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
!SpiceSimulationDiagnostics.#hasRequiredStringFields(
|
|
173
|
+
parsed,
|
|
174
|
+
definition.requiredFields
|
|
175
|
+
)
|
|
176
|
+
) {
|
|
177
|
+
diagnostics.push(
|
|
178
|
+
SpiceSimulationDiagnostics.#diagnostic({
|
|
179
|
+
code: 'spice_probe_metadata_invalid_shape',
|
|
180
|
+
lineNumber,
|
|
181
|
+
probeType: definition.probeType,
|
|
182
|
+
message:
|
|
183
|
+
SpiceSimulationDiagnostics.#probeTypeLabel(
|
|
184
|
+
definition.probeType
|
|
185
|
+
) +
|
|
186
|
+
' probe metadata comment is missing required string fields.'
|
|
187
|
+
})
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return diagnostics
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Returns true when parsed metadata contains all required string fields.
|
|
197
|
+
* @param {unknown} parsed Parsed metadata value.
|
|
198
|
+
* @param {string[]} requiredFields Required field names.
|
|
199
|
+
* @returns {boolean}
|
|
200
|
+
*/
|
|
201
|
+
static #hasRequiredStringFields(parsed, requiredFields) {
|
|
202
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
203
|
+
return false
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return requiredFields.every(
|
|
207
|
+
(field) => typeof parsed[field] === 'string'
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Returns a display label for a probe type.
|
|
213
|
+
* @param {string} probeType Probe metadata type.
|
|
214
|
+
* @returns {string}
|
|
215
|
+
*/
|
|
216
|
+
static #probeTypeLabel(probeType) {
|
|
217
|
+
return probeType === 'voltage' ? 'Voltage' : 'Current'
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Builds a warning diagnostic object.
|
|
222
|
+
* @param {{ code: string, lineNumber: number, message: string, probeType?: string }} options Diagnostic options.
|
|
223
|
+
* @returns {object}
|
|
224
|
+
*/
|
|
225
|
+
static #diagnostic(options) {
|
|
226
|
+
return {
|
|
227
|
+
severity: 'warning',
|
|
228
|
+
code: options.code,
|
|
229
|
+
lineNumber: options.lineNumber,
|
|
230
|
+
...(options.probeType ? { probeType: options.probeType } : {}),
|
|
231
|
+
message: options.message
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|