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.
Files changed (54) hide show
  1. package/AGENTS.md +5 -3
  2. package/README.md +21 -2
  3. package/docs/api.md +50 -4
  4. package/docs/model-format.md +21 -3
  5. package/package.json +3 -2
  6. package/spec/library-scope.md +4 -1
  7. package/src/core/CircuitJsonBomBuilder.mjs +143 -0
  8. package/src/core/CircuitJsonDocument.mjs +46 -13
  9. package/src/core/CircuitJsonElementValidator.mjs +990 -0
  10. package/src/core/CircuitJsonIndexer.mjs +773 -4
  11. package/src/core/CircuitJsonManufacturingBuilder.mjs +898 -0
  12. package/src/core/CircuitJsonManufacturingDownloadBuilder.mjs +196 -0
  13. package/src/core/CircuitJsonParser.mjs +22 -6
  14. package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +329 -0
  15. package/src/core/CircuitJsonPcbCopperGeometry.mjs +503 -0
  16. package/src/core/CircuitJsonPcbDrawingStyle.mjs +88 -0
  17. package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +172 -0
  18. package/src/core/CircuitJsonPcbNetMetadata.mjs +247 -0
  19. package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +70 -0
  20. package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +992 -0
  21. package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +872 -0
  22. package/src/core/CircuitJsonPcbPrimitiveFields.mjs +233 -0
  23. package/src/core/CircuitJsonPcbPrimitiveGeometry.mjs +142 -0
  24. package/src/core/CircuitJsonPcbPrimitiveGroups.mjs +305 -0
  25. package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +65 -0
  26. package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +895 -0
  27. package/src/core/CircuitJsonPcbTraceLengthModel.mjs +257 -0
  28. package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +683 -0
  29. package/src/core/CircuitJsonSourceMetadata.mjs +233 -0
  30. package/src/core/CircuitJsonSupportMatrixBuilder.mjs +481 -0
  31. package/src/core/CircuitJsonUnits.mjs +133 -8
  32. package/src/core/PcbBoundsSelectionModel.mjs +250 -0
  33. package/src/core/PcbCandidateSelectionModel.mjs +77 -0
  34. package/src/core/PcbDiagnosticFocusModel.mjs +423 -0
  35. package/src/core/PcbInteractionPrimitiveModel.mjs +560 -0
  36. package/src/core/SelectedPartCircuitJsonExportAdapter.mjs +335 -0
  37. package/src/core/spice/SpiceCompatibilityPreprocessor.mjs +139 -0
  38. package/src/core/spice/SpiceDirectiveParser.mjs +231 -0
  39. package/src/core/spice/SpiceFallbackSimulationEngine.mjs +168 -0
  40. package/src/core/spice/SpiceSimulationDiagnostics.mjs +234 -0
  41. package/src/core/spice/SpiceSimulationGraphBuilder.mjs +421 -0
  42. package/src/core/spice/SpiceSimulationGraphSummary.mjs +90 -0
  43. package/src/core/spice/SpiceSimulationService.mjs +92 -0
  44. package/src/core/spice/SpiceTimeSeriesNormalizer.mjs +132 -0
  45. package/src/index.mjs +8 -0
  46. package/src/renderers.mjs +29 -0
  47. package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +128 -0
  48. package/src/ui/CircuitJsonPcbSvgRenderer.mjs +964 -0
  49. package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +168 -0
  50. package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +138 -0
  51. package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +114 -0
  52. package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +130 -0
  53. package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +994 -0
  54. package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +439 -0
@@ -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
+ }