Skip to content

Commit 582e367

Browse files
ShiboSoftwareDevYour Name
andauthored
Refactor: Decompose circuitJsonToSpice into modular processors (#32)
Co-authored-by: Your Name <you@example.com>
1 parent ff3c347 commit 582e367

12 files changed

+774
-604
lines changed

lib/circuitJsonToSpice.ts

Lines changed: 53 additions & 604 deletions
Large diffs are not rendered by default.

lib/processors/helpers.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type { SimulationSwitch } from "circuit-json"
2+
3+
export function formatResistance(resistance: number): string {
4+
if (resistance >= 1e6) return `${resistance / 1e6}MEG`
5+
if (resistance >= 1e3) return `${resistance / 1e3}K`
6+
return resistance.toString()
7+
}
8+
9+
export function formatCapacitance(capacitance: number): string {
10+
if (capacitance >= 1e-3) return `${capacitance * 1e3}M`
11+
if (capacitance >= 1e-6) return `${capacitance * 1e6}U`
12+
if (capacitance >= 1e-9) return `${capacitance * 1e9}N`
13+
if (capacitance >= 1e-12) return `${capacitance * 1e12}P`
14+
return capacitance.toString()
15+
}
16+
17+
export function formatInductance(inductance: number): string {
18+
if (inductance >= 1) return inductance.toString()
19+
if (inductance >= 1e-3) return `${inductance * 1e3}m`
20+
if (inductance >= 1e-6) return `${inductance * 1e6}u`
21+
if (inductance >= 1e-9) return `${inductance * 1e9}n`
22+
if (inductance >= 1e-12) return `${inductance * 1e12}p`
23+
return inductance.toString()
24+
}
25+
26+
export function sanitizeIdentifier(value: string | undefined, prefix: string) {
27+
if (!value) return prefix
28+
const sanitized = value.replace(/[^A-Za-z0-9_]/g, "_")
29+
if (!sanitized) return prefix
30+
if (/^[0-9]/.test(sanitized)) {
31+
return `${prefix}_${sanitized}`
32+
}
33+
return sanitized
34+
}
35+
36+
export function buildSimulationSwitchControlValue(
37+
simulationSwitch: SimulationSwitch | undefined,
38+
) {
39+
const highVoltage = 5
40+
const lowVoltage = 0
41+
const riseTime = "1n"
42+
const fallTime = "1n"
43+
44+
if (!simulationSwitch) {
45+
return `DC ${lowVoltage}`
46+
}
47+
48+
const startsClosed = simulationSwitch.starts_closed ?? false
49+
const closesAt = simulationSwitch.closes_at ?? 0
50+
const opensAt = simulationSwitch.opens_at
51+
const switchingFrequency = simulationSwitch.switching_frequency
52+
53+
const [initialVoltage, pulsedVoltage] = startsClosed
54+
? [highVoltage, lowVoltage]
55+
: [lowVoltage, highVoltage]
56+
57+
if (switchingFrequency && switchingFrequency > 0) {
58+
const period = 1 / switchingFrequency
59+
const widthFromOpenClose =
60+
opensAt && opensAt > closesAt ? Math.min(opensAt - closesAt, period) : 0
61+
const pulseWidth =
62+
widthFromOpenClose > 0 ? widthFromOpenClose : Math.max(period / 2, 1e-9)
63+
64+
return `PULSE(${formatNumberForSpice(initialVoltage)} ${formatNumberForSpice(pulsedVoltage)} ${formatNumberForSpice(closesAt)} ${riseTime} ${fallTime} ${formatNumberForSpice(pulseWidth)} ${formatNumberForSpice(period)})`
65+
}
66+
67+
if (opensAt !== undefined && opensAt > closesAt) {
68+
const pulseWidth = Math.max(opensAt - closesAt, 1e-9)
69+
const period = closesAt + pulseWidth * 2
70+
71+
return `PULSE(${formatNumberForSpice(initialVoltage)} ${formatNumberForSpice(pulsedVoltage)} ${formatNumberForSpice(closesAt)} ${riseTime} ${fallTime} ${formatNumberForSpice(pulseWidth)} ${formatNumberForSpice(period)})`
72+
}
73+
74+
if (closesAt > 0) {
75+
const period = closesAt * 2
76+
const pulseWidth = Math.max(period / 2, 1e-9)
77+
return `PULSE(${formatNumberForSpice(initialVoltage)} ${formatNumberForSpice(pulsedVoltage)} ${formatNumberForSpice(closesAt)} ${riseTime} ${fallTime} ${formatNumberForSpice(pulseWidth)} ${formatNumberForSpice(period)})`
78+
}
79+
80+
return `DC ${startsClosed ? highVoltage : lowVoltage}`
81+
}
82+
83+
export function formatNumberForSpice(value: number) {
84+
if (!Number.isFinite(value)) return `${value}`
85+
if (value === 0) return "0"
86+
87+
const absValue = Math.abs(value)
88+
89+
if (absValue >= 1e3 || absValue <= 1e-3) {
90+
return Number(value.toExponential(6)).toString()
91+
}
92+
93+
return Number(value.toPrecision(6)).toString()
94+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import type { AnyCircuitElement } from "circuit-json"
2+
import { SpiceComponent } from "lib/spice-classes/SpiceComponent"
3+
import type { SpiceNetlist } from "lib/spice-classes/SpiceNetlist"
4+
import { CurrentSourceCommand } from "lib/spice-commands"
5+
6+
export const processSimulationCurrentSources = (
7+
netlist: SpiceNetlist,
8+
simulationCurrentSources: AnyCircuitElement[],
9+
nodeMap: Map<string, string>,
10+
) => {
11+
for (const simSource of simulationCurrentSources) {
12+
if (simSource.type !== "simulation_current_source") continue
13+
14+
if ((simSource as any).is_dc_source === false) {
15+
// AC/PULSE Source
16+
const positivePortId = (simSource as any).terminal1_source_port_id
17+
const negativePortId = (simSource as any).terminal2_source_port_id
18+
19+
if (positivePortId && negativePortId) {
20+
const positiveNode = nodeMap.get(positivePortId) || "0"
21+
const negativeNode = nodeMap.get(negativePortId) || "0"
22+
23+
let value = ""
24+
const wave_shape = (simSource as any).wave_shape
25+
if (wave_shape === "sinewave") {
26+
const i_offset = 0 // not provided
27+
const i_peak = ((simSource as any).peak_to_peak_current ?? 0) / 2
28+
const freq = (simSource as any).frequency ?? 0
29+
const delay = 0
30+
const damping_factor = 0
31+
const phase = (simSource as any).phase ?? 0
32+
if (freq > 0) {
33+
value = `SIN(${i_offset} ${i_peak} ${freq} ${delay} ${damping_factor} ${phase})`
34+
} else {
35+
value = `DC ${i_peak}`
36+
}
37+
} else if (wave_shape === "square") {
38+
const i_initial = 0
39+
const i_pulsed = (simSource as any).peak_to_peak_current ?? 0
40+
const freq = (simSource as any).frequency ?? 0
41+
const period_from_freq = freq === 0 ? Infinity : 1 / freq
42+
const period = (simSource as any).period ?? period_from_freq
43+
const duty_cycle = (simSource as any).duty_cycle ?? 0.5
44+
const pulse_width = period * duty_cycle
45+
const delay = 0
46+
const rise_time = "1n"
47+
const fall_time = "1n"
48+
value = `PULSE(${i_initial} ${i_pulsed} ${delay} ${rise_time} ${fall_time} ${pulse_width} ${period})`
49+
}
50+
51+
if (value) {
52+
const currentSourceCmd = new CurrentSourceCommand({
53+
name: (simSource as any).simulation_current_source_id,
54+
positiveNode,
55+
negativeNode,
56+
value,
57+
})
58+
59+
const spiceComponent = new SpiceComponent(
60+
(simSource as any).simulation_current_source_id,
61+
currentSourceCmd,
62+
[positiveNode, negativeNode],
63+
)
64+
netlist.addComponent(spiceComponent)
65+
}
66+
}
67+
} else {
68+
// DC Source
69+
const positivePortId = (simSource as any).positive_source_port_id
70+
const negativePortId = (simSource as any).negative_source_port_id
71+
72+
if (
73+
positivePortId &&
74+
negativePortId &&
75+
"current" in simSource &&
76+
(simSource as any).current !== undefined
77+
) {
78+
const positiveNode = nodeMap.get(positivePortId) || "0"
79+
const negativeNode = nodeMap.get(negativePortId) || "0"
80+
81+
const currentSourceCmd = new CurrentSourceCommand({
82+
name: (simSource as any).simulation_current_source_id,
83+
positiveNode,
84+
negativeNode,
85+
value: `DC ${(simSource as any).current}`,
86+
})
87+
88+
const spiceComponent = new SpiceComponent(
89+
(simSource as any).simulation_current_source_id,
90+
currentSourceCmd,
91+
[positiveNode, negativeNode],
92+
)
93+
netlist.addComponent(spiceComponent)
94+
}
95+
}
96+
}
97+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import type {
2+
AnyCircuitElement,
3+
SimulationVoltageProbe,
4+
SourceTrace,
5+
} from "circuit-json"
6+
import type { SpiceNetlist } from "lib/spice-classes/SpiceNetlist"
7+
import { formatNumberForSpice } from "./helpers"
8+
9+
export const processSimulationExperiment = (
10+
netlist: SpiceNetlist,
11+
simExperiment: AnyCircuitElement | undefined,
12+
simulationProbes: SimulationVoltageProbe[],
13+
sourceTraces: SourceTrace[],
14+
nodeMap: Map<string, string>,
15+
) => {
16+
if (!simExperiment) return
17+
18+
// Process simulation voltage probes
19+
if (simulationProbes.length > 0) {
20+
const nodesToProbe = new Set<string>()
21+
22+
const getPortIdFromNetId = (netId: string) => {
23+
const trace = sourceTraces.find((t) =>
24+
t.connected_source_net_ids.includes(netId),
25+
)
26+
return trace?.connected_source_port_ids[0]
27+
}
28+
29+
for (const probe of simulationProbes) {
30+
let signalPortId = probe.signal_input_source_port_id
31+
if (!signalPortId) {
32+
const signalNetId = probe.signal_input_source_net_id
33+
if (signalNetId) {
34+
signalPortId = getPortIdFromNetId(signalNetId)
35+
}
36+
}
37+
38+
if (!signalPortId) continue
39+
40+
const signalNodeName = nodeMap.get(signalPortId)
41+
if (!signalNodeName) continue
42+
43+
let referencePortId = probe.reference_input_source_port_id
44+
if (!referencePortId && probe.reference_input_source_net_id) {
45+
referencePortId = getPortIdFromNetId(
46+
probe.reference_input_source_net_id,
47+
)
48+
}
49+
50+
if (referencePortId) {
51+
const referenceNodeName = nodeMap.get(referencePortId)
52+
if (referenceNodeName && referenceNodeName !== "0") {
53+
nodesToProbe.add(`V(${signalNodeName},${referenceNodeName})`)
54+
} else if (signalNodeName !== "0") {
55+
nodesToProbe.add(`V(${signalNodeName})`)
56+
}
57+
} else {
58+
// Single-ended probe
59+
if (signalNodeName !== "0") {
60+
nodesToProbe.add(`V(${signalNodeName})`)
61+
}
62+
}
63+
}
64+
65+
if (
66+
nodesToProbe.size > 0 &&
67+
(simExperiment as any).experiment_type?.includes("transient")
68+
) {
69+
netlist.printStatements.push(`.PRINT TRAN ${[...nodesToProbe].join(" ")}`)
70+
}
71+
}
72+
73+
const timePerStep = (simExperiment as any).time_per_step
74+
const endTime = (simExperiment as any).end_time_ms
75+
const startTimeMs = (simExperiment as any).start_time_ms
76+
77+
if (timePerStep && endTime) {
78+
// circuit-json values are in ms, SPICE requires seconds
79+
const startTime = (startTimeMs ?? 0) / 1000
80+
81+
let tranCmd = `.tran ${formatNumberForSpice(
82+
timePerStep / 1000,
83+
)} ${formatNumberForSpice(endTime / 1000)}`
84+
if (startTime > 0) {
85+
tranCmd += ` ${formatNumberForSpice(startTime)}`
86+
}
87+
tranCmd += " UIC"
88+
netlist.tranCommand = tranCmd
89+
}
90+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import type { AnyCircuitElement } from "circuit-json"
2+
import { SpiceComponent } from "lib/spice-classes/SpiceComponent"
3+
import type { SpiceNetlist } from "lib/spice-classes/SpiceNetlist"
4+
import { VoltageSourceCommand } from "lib/spice-commands"
5+
6+
export const processSimulationVoltageSources = (
7+
netlist: SpiceNetlist,
8+
simulationVoltageSources: AnyCircuitElement[],
9+
nodeMap: Map<string, string>,
10+
) => {
11+
for (const simSource of simulationVoltageSources) {
12+
if (simSource.type !== "simulation_voltage_source") continue
13+
14+
if ((simSource as any).is_dc_source === false) {
15+
// AC Source
16+
if (
17+
"terminal1_source_port_id" in simSource &&
18+
"terminal2_source_port_id" in simSource &&
19+
(simSource as any).terminal1_source_port_id &&
20+
(simSource as any).terminal2_source_port_id
21+
) {
22+
const positiveNode =
23+
nodeMap.get((simSource as any).terminal1_source_port_id) || "0"
24+
const negativeNode =
25+
nodeMap.get((simSource as any).terminal2_source_port_id) || "0"
26+
27+
let value = ""
28+
const wave_shape = (simSource as any).wave_shape
29+
if (wave_shape === "sinewave") {
30+
const v_offset = 0 // not provided in circuitJson
31+
const v_peak = (simSource as any).voltage ?? 0
32+
const freq = (simSource as any).frequency ?? 0
33+
const delay = 0 // not provided in circuitJson
34+
const damping_factor = 0 // not provided in circuitJson
35+
const phase = (simSource as any).phase ?? 0
36+
if (freq > 0) {
37+
value = `SIN(${v_offset} ${v_peak} ${freq} ${delay} ${damping_factor} ${phase})`
38+
} else {
39+
value = `DC ${(simSource as any).voltage ?? 0}`
40+
}
41+
} else if (wave_shape === "square") {
42+
const v_initial = 0
43+
const v_pulsed = (simSource as any).voltage ?? 0
44+
const freq = (simSource as any).frequency ?? 0
45+
const period_from_freq = freq === 0 ? Infinity : 1 / freq
46+
const period = (simSource as any).period ?? period_from_freq
47+
const duty_cycle = (simSource as any).duty_cycle ?? 0.5
48+
const pulse_width = period * duty_cycle
49+
const delay = 0
50+
const rise_time = "1n"
51+
const fall_time = "1n"
52+
value = `PULSE(${v_initial} ${v_pulsed} ${delay} ${rise_time} ${fall_time} ${pulse_width} ${period})`
53+
} else if ((simSource as any).voltage !== undefined) {
54+
value = `DC ${(simSource as any).voltage}`
55+
}
56+
57+
if (value) {
58+
const voltageSourceCmd = new VoltageSourceCommand({
59+
name: simSource.simulation_voltage_source_id,
60+
positiveNode,
61+
negativeNode,
62+
value,
63+
})
64+
65+
const spiceComponent = new SpiceComponent(
66+
simSource.simulation_voltage_source_id,
67+
voltageSourceCmd,
68+
[positiveNode, negativeNode],
69+
)
70+
netlist.addComponent(spiceComponent)
71+
}
72+
}
73+
} else {
74+
// DC Source (is_dc_source is true or undefined)
75+
const positivePortId =
76+
(simSource as any).positive_source_port_id ??
77+
(simSource as any).terminal1_source_port_id
78+
const negativePortId =
79+
(simSource as any).negative_source_port_id ??
80+
(simSource as any).terminal2_source_port_id
81+
82+
if (
83+
positivePortId &&
84+
negativePortId &&
85+
"voltage" in simSource &&
86+
(simSource as any).voltage !== undefined
87+
) {
88+
const positiveNode = nodeMap.get(positivePortId) || "0"
89+
const negativeNode = nodeMap.get(negativePortId) || "0"
90+
91+
const voltageSourceCmd = new VoltageSourceCommand({
92+
name: simSource.simulation_voltage_source_id,
93+
positiveNode,
94+
negativeNode,
95+
value: `DC ${(simSource as any).voltage}`,
96+
})
97+
98+
const spiceComponent = new SpiceComponent(
99+
simSource.simulation_voltage_source_id,
100+
voltageSourceCmd,
101+
[positiveNode, negativeNode],
102+
)
103+
netlist.addComponent(spiceComponent)
104+
}
105+
}
106+
}
107+
}

0 commit comments

Comments
 (0)