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,259 @@
|
|
|
1
|
+
import { CircuitJsonElementValidator } from './CircuitJsonElementValidator.mjs'
|
|
2
|
+
|
|
3
|
+
const PCB_RENDERED_TYPES = new Set([
|
|
4
|
+
'pcb_board',
|
|
5
|
+
'pcb_breakout_point',
|
|
6
|
+
'pcb_component',
|
|
7
|
+
'pcb_copper_pour',
|
|
8
|
+
'pcb_copper_text',
|
|
9
|
+
'pcb_courtyard',
|
|
10
|
+
'pcb_courtyard_circle',
|
|
11
|
+
'pcb_courtyard_outline',
|
|
12
|
+
'pcb_courtyard_pill',
|
|
13
|
+
'pcb_courtyard_polygon',
|
|
14
|
+
'pcb_courtyard_rect',
|
|
15
|
+
'pcb_cutout',
|
|
16
|
+
'pcb_fabrication_note_dimension',
|
|
17
|
+
'pcb_fabrication_note_path',
|
|
18
|
+
'pcb_fabrication_note_rect',
|
|
19
|
+
'pcb_fabrication_note_text',
|
|
20
|
+
'pcb_ground_plane',
|
|
21
|
+
'pcb_ground_plane_region',
|
|
22
|
+
'pcb_group',
|
|
23
|
+
'pcb_hole',
|
|
24
|
+
'pcb_keepout',
|
|
25
|
+
'pcb_note_dimension',
|
|
26
|
+
'pcb_note_line',
|
|
27
|
+
'pcb_note_path',
|
|
28
|
+
'pcb_note_rect',
|
|
29
|
+
'pcb_note_text',
|
|
30
|
+
'pcb_panel',
|
|
31
|
+
'pcb_plated_hole',
|
|
32
|
+
'pcb_port',
|
|
33
|
+
'pcb_silkscreen_circle',
|
|
34
|
+
'pcb_silkscreen_graphic',
|
|
35
|
+
'pcb_silkscreen_line',
|
|
36
|
+
'pcb_silkscreen_oval',
|
|
37
|
+
'pcb_silkscreen_path',
|
|
38
|
+
'pcb_silkscreen_pill',
|
|
39
|
+
'pcb_silkscreen_rect',
|
|
40
|
+
'pcb_silkscreen_text',
|
|
41
|
+
'pcb_smtpad',
|
|
42
|
+
'pcb_solder_paste',
|
|
43
|
+
'pcb_text',
|
|
44
|
+
'pcb_thermal_spoke',
|
|
45
|
+
'pcb_trace',
|
|
46
|
+
'pcb_trace_hint',
|
|
47
|
+
'pcb_via'
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
const SCHEMATIC_RENDERED_TYPES = new Set([
|
|
51
|
+
'schematic_arc',
|
|
52
|
+
'schematic_box',
|
|
53
|
+
'schematic_circle',
|
|
54
|
+
'schematic_component',
|
|
55
|
+
'schematic_debug_object',
|
|
56
|
+
'schematic_group',
|
|
57
|
+
'schematic_line',
|
|
58
|
+
'schematic_net_label',
|
|
59
|
+
'schematic_path',
|
|
60
|
+
'schematic_port',
|
|
61
|
+
'schematic_rect',
|
|
62
|
+
'schematic_sheet',
|
|
63
|
+
'schematic_symbol',
|
|
64
|
+
'schematic_table',
|
|
65
|
+
'schematic_table_cell',
|
|
66
|
+
'schematic_text',
|
|
67
|
+
'schematic_trace',
|
|
68
|
+
'schematic_voltage_probe'
|
|
69
|
+
])
|
|
70
|
+
|
|
71
|
+
const ROUTING_DSN_TYPES = new Set([
|
|
72
|
+
'pcb_board',
|
|
73
|
+
'pcb_component',
|
|
74
|
+
'pcb_smtpad',
|
|
75
|
+
'pcb_trace',
|
|
76
|
+
'pcb_via',
|
|
77
|
+
'pcb_plated_hole',
|
|
78
|
+
'source_net'
|
|
79
|
+
])
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Builds document-level support coverage reports from known element metadata.
|
|
83
|
+
*/
|
|
84
|
+
export class CircuitJsonSupportMatrixBuilder {
|
|
85
|
+
/**
|
|
86
|
+
* Builds a support matrix for the known schema snapshot and present rows.
|
|
87
|
+
* @param {object[]} [circuitJson] Parsed element array.
|
|
88
|
+
* @returns {{ sourceFormat: string, totals: object, rows: object[], gaps: object[] }}
|
|
89
|
+
*/
|
|
90
|
+
static build(circuitJson = []) {
|
|
91
|
+
const elements = Array.isArray(circuitJson) ? circuitJson : []
|
|
92
|
+
const presentTypes = new Set(
|
|
93
|
+
elements
|
|
94
|
+
.map((element) => String(element?.type || ''))
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
)
|
|
97
|
+
const rows = CircuitJsonElementValidator.knownElementTypes().map(
|
|
98
|
+
(type) =>
|
|
99
|
+
CircuitJsonSupportMatrixBuilder.#row(
|
|
100
|
+
type,
|
|
101
|
+
presentTypes.has(type)
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
sourceFormat: 'circuitjson',
|
|
107
|
+
totals: CircuitJsonSupportMatrixBuilder.#totals(rows, presentTypes),
|
|
108
|
+
rows,
|
|
109
|
+
gaps: rows.flatMap((row) =>
|
|
110
|
+
CircuitJsonSupportMatrixBuilder.#gaps(row)
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Builds one matrix row.
|
|
117
|
+
* @param {string} type Element type.
|
|
118
|
+
* @param {boolean} present Whether the current document contains the type.
|
|
119
|
+
* @returns {object}
|
|
120
|
+
*/
|
|
121
|
+
static #row(type, present) {
|
|
122
|
+
const capabilities = CircuitJsonSupportMatrixBuilder.#capabilities(type)
|
|
123
|
+
return {
|
|
124
|
+
type,
|
|
125
|
+
family: CircuitJsonSupportMatrixBuilder.#family(type),
|
|
126
|
+
present,
|
|
127
|
+
status: CircuitJsonSupportMatrixBuilder.#status(capabilities),
|
|
128
|
+
capabilities
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Resolves capability labels for one type.
|
|
134
|
+
* @param {string} type Element type.
|
|
135
|
+
* @returns {Record<string, string>}
|
|
136
|
+
*/
|
|
137
|
+
static #capabilities(type) {
|
|
138
|
+
const capabilities = {
|
|
139
|
+
validation: 'known',
|
|
140
|
+
parser: 'preserved',
|
|
141
|
+
indexer: 'indexed',
|
|
142
|
+
diagnostics: CircuitJsonSupportMatrixBuilder.#diagnostics(type),
|
|
143
|
+
schematic: SCHEMATIC_RENDERED_TYPES.has(type) ? 'rendered' : 'none',
|
|
144
|
+
pcb: PCB_RENDERED_TYPES.has(type) ? 'rendered' : 'none',
|
|
145
|
+
bom: type === 'source_component' ? 'grouped' : 'none',
|
|
146
|
+
manufacturing: CircuitJsonSupportMatrixBuilder.#manufacturing(type),
|
|
147
|
+
simulation: type.startsWith('simulation_') ? 'preserved' : 'none'
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (type === 'pcb_component') {
|
|
151
|
+
capabilities.manufacturing = 'pick-and-place'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return capabilities
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Resolves diagnostic support for one type.
|
|
159
|
+
* @param {string} type Element type.
|
|
160
|
+
* @returns {string}
|
|
161
|
+
*/
|
|
162
|
+
static #diagnostics(type) {
|
|
163
|
+
return /(?:error|warning)/u.test(type) ? 'normalized' : 'none'
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Resolves manufacturing support for one type.
|
|
168
|
+
* @param {string} type Element type.
|
|
169
|
+
* @returns {string}
|
|
170
|
+
*/
|
|
171
|
+
static #manufacturing(type) {
|
|
172
|
+
if (ROUTING_DSN_TYPES.has(type)) return 'routing-dsn'
|
|
173
|
+
return 'none'
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Resolves an overall support status.
|
|
178
|
+
* @param {Record<string, string>} capabilities Capability labels.
|
|
179
|
+
* @returns {'full' | 'partial' | 'metadata'}
|
|
180
|
+
*/
|
|
181
|
+
static #status(capabilities) {
|
|
182
|
+
if (
|
|
183
|
+
capabilities.pcb === 'rendered' ||
|
|
184
|
+
capabilities.schematic === 'rendered' ||
|
|
185
|
+
capabilities.diagnostics === 'normalized'
|
|
186
|
+
) {
|
|
187
|
+
return capabilities.manufacturing === 'routing-dsn'
|
|
188
|
+
? 'partial'
|
|
189
|
+
: 'full'
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return 'metadata'
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Builds gap rows for present partially supported capabilities.
|
|
197
|
+
* @param {object} row Matrix row.
|
|
198
|
+
* @returns {object[]}
|
|
199
|
+
*/
|
|
200
|
+
static #gaps(row) {
|
|
201
|
+
if (!row.present) return []
|
|
202
|
+
if (row.capabilities.manufacturing === 'routing-dsn') {
|
|
203
|
+
return [
|
|
204
|
+
{
|
|
205
|
+
type: row.type,
|
|
206
|
+
capability: 'manufacturing',
|
|
207
|
+
status: 'partial',
|
|
208
|
+
detail: 'Routing exchange metadata is generated without full fabrication packaging.'
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
if (row.status === 'metadata') {
|
|
213
|
+
return [
|
|
214
|
+
{
|
|
215
|
+
type: row.type,
|
|
216
|
+
capability: 'rendering',
|
|
217
|
+
status: 'metadata',
|
|
218
|
+
detail: 'The element is preserved for downstream consumers.'
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
return []
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Builds aggregate matrix counts.
|
|
227
|
+
* @param {object[]} rows Matrix rows.
|
|
228
|
+
* @param {Set<string>} presentTypes Present element types.
|
|
229
|
+
* @returns {object}
|
|
230
|
+
*/
|
|
231
|
+
static #totals(rows, presentTypes) {
|
|
232
|
+
return {
|
|
233
|
+
knownElementTypes: rows.length,
|
|
234
|
+
presentElementTypes: rows.filter((row) => row.present).length,
|
|
235
|
+
renderedElementTypes: rows.filter(
|
|
236
|
+
(row) =>
|
|
237
|
+
row.present &&
|
|
238
|
+
(row.capabilities.pcb === 'rendered' ||
|
|
239
|
+
row.capabilities.schematic === 'rendered')
|
|
240
|
+
).length,
|
|
241
|
+
diagnosticElementTypes: rows.filter(
|
|
242
|
+
(row) =>
|
|
243
|
+
row.present && row.capabilities.diagnostics === 'normalized'
|
|
244
|
+
).length,
|
|
245
|
+
unknownPresentElementTypes: [...presentTypes].filter(
|
|
246
|
+
(type) => !rows.some((row) => row.type === type)
|
|
247
|
+
).length
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Resolves the top-level element family.
|
|
253
|
+
* @param {string} type Element type.
|
|
254
|
+
* @returns {string}
|
|
255
|
+
*/
|
|
256
|
+
static #family(type) {
|
|
257
|
+
return String(type || '').split('_')[0] || 'unknown'
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -1,9 +1,114 @@
|
|
|
1
1
|
const MILS_PER_MM = 39.37007874015748
|
|
2
|
+
const LENGTH_FACTORS_TO_MM = new Map([
|
|
3
|
+
['mm', 1],
|
|
4
|
+
['millimeter', 1],
|
|
5
|
+
['millimeters', 1],
|
|
6
|
+
['cm', 10],
|
|
7
|
+
['centimeter', 10],
|
|
8
|
+
['centimeters', 10],
|
|
9
|
+
['m', 1000],
|
|
10
|
+
['meter', 1000],
|
|
11
|
+
['meters', 1000],
|
|
12
|
+
['in', 25.4],
|
|
13
|
+
['inch', 25.4],
|
|
14
|
+
['inches', 25.4],
|
|
15
|
+
['mil', 0.0254],
|
|
16
|
+
['mils', 0.0254],
|
|
17
|
+
['um', 0.001],
|
|
18
|
+
['micrometer', 0.001],
|
|
19
|
+
['micrometers', 0.001]
|
|
20
|
+
])
|
|
21
|
+
const ANGLE_FACTORS_TO_DEG = new Map([
|
|
22
|
+
['deg', 1],
|
|
23
|
+
['degree', 1],
|
|
24
|
+
['degrees', 1],
|
|
25
|
+
['rad', 180 / Math.PI],
|
|
26
|
+
['radian', 180 / Math.PI],
|
|
27
|
+
['radians', 180 / Math.PI]
|
|
28
|
+
])
|
|
2
29
|
|
|
3
30
|
/**
|
|
4
31
|
* Unit helpers for CircuitJSON's millimeter-based PCB dimensions.
|
|
5
32
|
*/
|
|
6
33
|
export class CircuitJsonUnits {
|
|
34
|
+
/**
|
|
35
|
+
* Converts a length value to millimeters.
|
|
36
|
+
* @param {unknown} value Length candidate.
|
|
37
|
+
* @param {number} [fallback] Fallback millimeter value.
|
|
38
|
+
* @returns {number}
|
|
39
|
+
*/
|
|
40
|
+
static length(value, fallback = 0) {
|
|
41
|
+
return (
|
|
42
|
+
CircuitJsonUnits.optionalLength(value) ??
|
|
43
|
+
CircuitJsonUnits.#round(fallback)
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Converts a length value to millimeters, or null when invalid.
|
|
49
|
+
* @param {unknown} value Length candidate.
|
|
50
|
+
* @returns {number | null}
|
|
51
|
+
*/
|
|
52
|
+
static optionalLength(value) {
|
|
53
|
+
return CircuitJsonUnits.#parseUnitValue(value, LENGTH_FACTORS_TO_MM)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Converts an angle value to degrees.
|
|
58
|
+
* @param {unknown} value Angle candidate.
|
|
59
|
+
* @param {number} [fallback] Fallback degree value.
|
|
60
|
+
* @returns {number}
|
|
61
|
+
*/
|
|
62
|
+
static angle(value, fallback = 0) {
|
|
63
|
+
return (
|
|
64
|
+
CircuitJsonUnits.optionalAngle(value) ??
|
|
65
|
+
CircuitJsonUnits.#round(fallback)
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Converts an angle value to degrees, or null when invalid.
|
|
71
|
+
* @param {unknown} value Angle candidate.
|
|
72
|
+
* @returns {number | null}
|
|
73
|
+
*/
|
|
74
|
+
static optionalAngle(value) {
|
|
75
|
+
return CircuitJsonUnits.#parseUnitValue(value, ANGLE_FACTORS_TO_DEG)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Converts a point to normalized millimeter coordinates.
|
|
80
|
+
* @param {{ x?: unknown, y?: unknown } | null | undefined} point Point.
|
|
81
|
+
* @returns {{ x: number, y: number }}
|
|
82
|
+
*/
|
|
83
|
+
static point(point) {
|
|
84
|
+
return {
|
|
85
|
+
x: CircuitJsonUnits.length(point?.x, 0),
|
|
86
|
+
y: CircuitJsonUnits.length(point?.y, 0)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Converts a point to normalized millimeter coordinates when valid.
|
|
92
|
+
* @param {{ x?: unknown, y?: unknown } | null | undefined} point Point.
|
|
93
|
+
* @returns {{ x: number, y: number } | null}
|
|
94
|
+
*/
|
|
95
|
+
static optionalPoint(point) {
|
|
96
|
+
const x = CircuitJsonUnits.optionalLength(point?.x)
|
|
97
|
+
const y = CircuitJsonUnits.optionalLength(point?.y)
|
|
98
|
+
return x === null || y === null ? null : { x, y }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Converts a size to normalized millimeter dimensions.
|
|
103
|
+
* @param {{ width?: unknown, height?: unknown } | null | undefined} size Size.
|
|
104
|
+
* @returns {{ width: number, height: number } | null}
|
|
105
|
+
*/
|
|
106
|
+
static optionalSize(size) {
|
|
107
|
+
const width = CircuitJsonUnits.optionalLength(size?.width)
|
|
108
|
+
const height = CircuitJsonUnits.optionalLength(size?.height)
|
|
109
|
+
return width === null || height === null ? null : { width, height }
|
|
110
|
+
}
|
|
111
|
+
|
|
7
112
|
/**
|
|
8
113
|
* Converts millimeters to mils.
|
|
9
114
|
* @param {unknown} value Millimeter value.
|
|
@@ -12,7 +117,7 @@ export class CircuitJsonUnits {
|
|
|
12
117
|
*/
|
|
13
118
|
static mmToMil(value, fallback = 0) {
|
|
14
119
|
return CircuitJsonUnits.#round(
|
|
15
|
-
CircuitJsonUnits
|
|
120
|
+
CircuitJsonUnits.length(value, fallback) * MILS_PER_MM
|
|
16
121
|
)
|
|
17
122
|
}
|
|
18
123
|
|
|
@@ -29,14 +134,34 @@ export class CircuitJsonUnits {
|
|
|
29
134
|
}
|
|
30
135
|
|
|
31
136
|
/**
|
|
32
|
-
*
|
|
33
|
-
* @param {unknown} value
|
|
34
|
-
* @param {number}
|
|
35
|
-
* @returns {number}
|
|
137
|
+
* Parses one numeric value with an optional unit suffix.
|
|
138
|
+
* @param {unknown} value Value candidate.
|
|
139
|
+
* @param {Map<string, number>} unitFactors Unit factor lookup.
|
|
140
|
+
* @returns {number | null}
|
|
36
141
|
*/
|
|
37
|
-
static #
|
|
38
|
-
|
|
39
|
-
|
|
142
|
+
static #parseUnitValue(value, unitFactors) {
|
|
143
|
+
if (typeof value === 'number') {
|
|
144
|
+
return Number.isFinite(value)
|
|
145
|
+
? CircuitJsonUnits.#round(value)
|
|
146
|
+
: null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const text = String(value ?? '').trim()
|
|
150
|
+
if (!text) return null
|
|
151
|
+
|
|
152
|
+
const match = text.match(
|
|
153
|
+
/^([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)\s*([a-z]+)?$/iu
|
|
154
|
+
)
|
|
155
|
+
if (!match) return null
|
|
156
|
+
|
|
157
|
+
const number = Number(match[1])
|
|
158
|
+
if (!Number.isFinite(number)) return null
|
|
159
|
+
|
|
160
|
+
const unit = String(match[2] || '').toLowerCase()
|
|
161
|
+
const factor = unit ? unitFactors.get(unit) : 1
|
|
162
|
+
if (!Number.isFinite(factor)) return null
|
|
163
|
+
|
|
164
|
+
return CircuitJsonUnits.#round(number * factor)
|
|
40
165
|
}
|
|
41
166
|
|
|
42
167
|
/**
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const PSPICE_NUMBER_TOKEN = String.raw`([+-]?(?:(?:\d+(?:\.\d*)?)|(?:\.\d+))(?:[eE][+-]?\d+)?)`
|
|
2
|
+
const PSPICE_COMPARISON_OPERATOR = '(?:<=|>=|==|!=|(?<![!<>=])=(?!=)|<|>)'
|
|
3
|
+
const PSPICE_COMPARISON_OPERAND = String.raw`(?:V\s*\([^)]*\)|\{[^}\r\n]+\}|${PSPICE_NUMBER_TOKEN}(?:[a-zA-Z]+)?|[A-Za-z_][\w.$]*)`
|
|
4
|
+
const PSPICE_COMPARISON_EXPRESSION = String.raw`${PSPICE_COMPARISON_OPERAND}\s*${PSPICE_COMPARISON_OPERATOR}\s*${PSPICE_COMPARISON_OPERAND}`
|
|
5
|
+
const PSPICE_COMPARISON_BEFORE_CARET_PATTERN = new RegExp(
|
|
6
|
+
String.raw`${PSPICE_COMPARISON_EXPRESSION}\s*$`,
|
|
7
|
+
'i'
|
|
8
|
+
)
|
|
9
|
+
const PSPICE_COMPARISON_AFTER_CARET_PATTERN = new RegExp(
|
|
10
|
+
String.raw`^\s*\+?\s*${PSPICE_COMPARISON_EXPRESSION}`,
|
|
11
|
+
'i'
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Rewrites narrow, well-understood SPICE compatibility syntax.
|
|
16
|
+
*/
|
|
17
|
+
export class SpiceCompatibilityPreprocessor {
|
|
18
|
+
/**
|
|
19
|
+
* Returns a netlist with supported compatibility syntax rewritten.
|
|
20
|
+
* @param {string} spiceString Raw SPICE netlist text.
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
static rewrite(spiceString) {
|
|
24
|
+
return SpiceCompatibilityPreprocessor.#rewriteValueBooleanCarets(
|
|
25
|
+
SpiceCompatibilityPreprocessor.#rewriteResistorTemperaturePairs(
|
|
26
|
+
spiceString
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Rewrites resistor TC pairs to separate TC1 and TC2 assignments.
|
|
33
|
+
* @param {string} spiceString Raw SPICE netlist text.
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
static #rewriteResistorTemperaturePairs(spiceString) {
|
|
37
|
+
return String(spiceString || '')
|
|
38
|
+
.split(/\r?\n/)
|
|
39
|
+
.map((line) => {
|
|
40
|
+
if (!/^\s*r/i.test(line)) return line
|
|
41
|
+
|
|
42
|
+
return line.replace(
|
|
43
|
+
new RegExp(
|
|
44
|
+
String.raw`\bTC\s*=\s*${PSPICE_NUMBER_TOKEN}\s*,\s*${PSPICE_NUMBER_TOKEN}\b`,
|
|
45
|
+
'gi'
|
|
46
|
+
),
|
|
47
|
+
'TC1=$1 TC2=$2'
|
|
48
|
+
)
|
|
49
|
+
})
|
|
50
|
+
.join('\n')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Rewrites boolean caret operators inside VALUE expression blocks.
|
|
55
|
+
* @param {string} spiceString Raw SPICE netlist text.
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
static #rewriteValueBooleanCarets(spiceString) {
|
|
59
|
+
let result = ''
|
|
60
|
+
let cursor = 0
|
|
61
|
+
const valueStartPattern = /\bVALUE\s*\{/gi
|
|
62
|
+
|
|
63
|
+
for (;;) {
|
|
64
|
+
valueStartPattern.lastIndex = cursor
|
|
65
|
+
const match = valueStartPattern.exec(spiceString)
|
|
66
|
+
if (!match) break
|
|
67
|
+
|
|
68
|
+
const blockStart = match.index
|
|
69
|
+
const firstBraceIndex = spiceString.indexOf('{', blockStart)
|
|
70
|
+
const blockEnd =
|
|
71
|
+
SpiceCompatibilityPreprocessor.#findBalancedBlockEnd(
|
|
72
|
+
spiceString,
|
|
73
|
+
firstBraceIndex
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if (blockEnd === -1) break
|
|
77
|
+
|
|
78
|
+
result += spiceString.slice(cursor, blockStart)
|
|
79
|
+
const block = spiceString.slice(blockStart, blockEnd)
|
|
80
|
+
result += block.replace(/\s+\^\s+/g, (operator, offset, full) => {
|
|
81
|
+
if (
|
|
82
|
+
SpiceCompatibilityPreprocessor.#isValueBooleanCaret(
|
|
83
|
+
full,
|
|
84
|
+
offset,
|
|
85
|
+
operator.length
|
|
86
|
+
)
|
|
87
|
+
) {
|
|
88
|
+
return operator.replace('^', '!=')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return operator
|
|
92
|
+
})
|
|
93
|
+
cursor = blockEnd
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result + spiceString.slice(cursor)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Finds the exclusive end offset of a balanced brace block.
|
|
101
|
+
* @param {string} text Source text.
|
|
102
|
+
* @param {number} firstBraceIndex Offset of the opening brace.
|
|
103
|
+
* @returns {number}
|
|
104
|
+
*/
|
|
105
|
+
static #findBalancedBlockEnd(text, firstBraceIndex) {
|
|
106
|
+
if (firstBraceIndex < 0) return -1
|
|
107
|
+
|
|
108
|
+
let depth = 0
|
|
109
|
+
for (let index = firstBraceIndex; index < text.length; index += 1) {
|
|
110
|
+
const character = text[index]
|
|
111
|
+
if (character === '{') {
|
|
112
|
+
depth += 1
|
|
113
|
+
} else if (character === '}') {
|
|
114
|
+
depth -= 1
|
|
115
|
+
if (depth === 0) return index + 1
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return -1
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Returns true when a caret separates two comparison expressions.
|
|
124
|
+
* @param {string} block VALUE block text.
|
|
125
|
+
* @param {number} caretOffset Caret operator offset.
|
|
126
|
+
* @param {number} operatorLength Operator token length.
|
|
127
|
+
* @returns {boolean}
|
|
128
|
+
*/
|
|
129
|
+
static #isValueBooleanCaret(block, caretOffset, operatorLength) {
|
|
130
|
+
return (
|
|
131
|
+
PSPICE_COMPARISON_BEFORE_CARET_PATTERN.test(
|
|
132
|
+
block.slice(0, caretOffset)
|
|
133
|
+
) &&
|
|
134
|
+
PSPICE_COMPARISON_AFTER_CARET_PATTERN.test(
|
|
135
|
+
block.slice(caretOffset + operatorLength)
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
}
|