Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
657 changes: 53 additions & 604 deletions lib/circuitJsonToSpice.ts

Large diffs are not rendered by default.

94 changes: 94 additions & 0 deletions lib/processors/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { SimulationSwitch } from "circuit-json"

export function formatResistance(resistance: number): string {
if (resistance >= 1e6) return `${resistance / 1e6}MEG`
if (resistance >= 1e3) return `${resistance / 1e3}K`
return resistance.toString()
}

export function formatCapacitance(capacitance: number): string {
if (capacitance >= 1e-3) return `${capacitance * 1e3}M`
if (capacitance >= 1e-6) return `${capacitance * 1e6}U`
if (capacitance >= 1e-9) return `${capacitance * 1e9}N`
if (capacitance >= 1e-12) return `${capacitance * 1e12}P`
return capacitance.toString()
}

export function formatInductance(inductance: number): string {
if (inductance >= 1) return inductance.toString()
if (inductance >= 1e-3) return `${inductance * 1e3}m`
if (inductance >= 1e-6) return `${inductance * 1e6}u`
if (inductance >= 1e-9) return `${inductance * 1e9}n`
if (inductance >= 1e-12) return `${inductance * 1e12}p`
return inductance.toString()
}

export function sanitizeIdentifier(value: string | undefined, prefix: string) {
if (!value) return prefix
const sanitized = value.replace(/[^A-Za-z0-9_]/g, "_")
if (!sanitized) return prefix
if (/^[0-9]/.test(sanitized)) {
return `${prefix}_${sanitized}`
}
return sanitized
}

export function buildSimulationSwitchControlValue(
simulationSwitch: SimulationSwitch | undefined,
) {
const highVoltage = 5
const lowVoltage = 0
const riseTime = "1n"
const fallTime = "1n"

if (!simulationSwitch) {
return `DC ${lowVoltage}`
}

const startsClosed = simulationSwitch.starts_closed ?? false
const closesAt = simulationSwitch.closes_at ?? 0
const opensAt = simulationSwitch.opens_at
const switchingFrequency = simulationSwitch.switching_frequency

const [initialVoltage, pulsedVoltage] = startsClosed
? [highVoltage, lowVoltage]
: [lowVoltage, highVoltage]

if (switchingFrequency && switchingFrequency > 0) {
const period = 1 / switchingFrequency
const widthFromOpenClose =
opensAt && opensAt > closesAt ? Math.min(opensAt - closesAt, period) : 0
const pulseWidth =
widthFromOpenClose > 0 ? widthFromOpenClose : Math.max(period / 2, 1e-9)

return `PULSE(${formatNumberForSpice(initialVoltage)} ${formatNumberForSpice(pulsedVoltage)} ${formatNumberForSpice(closesAt)} ${riseTime} ${fallTime} ${formatNumberForSpice(pulseWidth)} ${formatNumberForSpice(period)})`
}

if (opensAt !== undefined && opensAt > closesAt) {
const pulseWidth = Math.max(opensAt - closesAt, 1e-9)
const period = closesAt + pulseWidth * 2

return `PULSE(${formatNumberForSpice(initialVoltage)} ${formatNumberForSpice(pulsedVoltage)} ${formatNumberForSpice(closesAt)} ${riseTime} ${fallTime} ${formatNumberForSpice(pulseWidth)} ${formatNumberForSpice(period)})`
}

if (closesAt > 0) {
const period = closesAt * 2
const pulseWidth = Math.max(period / 2, 1e-9)
return `PULSE(${formatNumberForSpice(initialVoltage)} ${formatNumberForSpice(pulsedVoltage)} ${formatNumberForSpice(closesAt)} ${riseTime} ${fallTime} ${formatNumberForSpice(pulseWidth)} ${formatNumberForSpice(period)})`
}

return `DC ${startsClosed ? highVoltage : lowVoltage}`
}

export function formatNumberForSpice(value: number) {
if (!Number.isFinite(value)) return `${value}`
if (value === 0) return "0"

const absValue = Math.abs(value)

if (absValue >= 1e3 || absValue <= 1e-3) {
return Number(value.toExponential(6)).toString()
}

return Number(value.toPrecision(6)).toString()
}
97 changes: 97 additions & 0 deletions lib/processors/process-simulation-current-sources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { AnyCircuitElement } from "circuit-json"
import { SpiceComponent } from "lib/spice-classes/SpiceComponent"
import type { SpiceNetlist } from "lib/spice-classes/SpiceNetlist"
import { CurrentSourceCommand } from "lib/spice-commands"

export const processSimulationCurrentSources = (
netlist: SpiceNetlist,
simulationCurrentSources: AnyCircuitElement[],
nodeMap: Map<string, string>,
) => {
for (const simSource of simulationCurrentSources) {
if (simSource.type !== "simulation_current_source") continue

if ((simSource as any).is_dc_source === false) {
// AC/PULSE Source
const positivePortId = (simSource as any).terminal1_source_port_id
const negativePortId = (simSource as any).terminal2_source_port_id

if (positivePortId && negativePortId) {
const positiveNode = nodeMap.get(positivePortId) || "0"
const negativeNode = nodeMap.get(negativePortId) || "0"

let value = ""
const wave_shape = (simSource as any).wave_shape
if (wave_shape === "sinewave") {
const i_offset = 0 // not provided
const i_peak = ((simSource as any).peak_to_peak_current ?? 0) / 2
const freq = (simSource as any).frequency ?? 0
const delay = 0
const damping_factor = 0
const phase = (simSource as any).phase ?? 0
if (freq > 0) {
value = `SIN(${i_offset} ${i_peak} ${freq} ${delay} ${damping_factor} ${phase})`
} else {
value = `DC ${i_peak}`
}
} else if (wave_shape === "square") {
const i_initial = 0
const i_pulsed = (simSource as any).peak_to_peak_current ?? 0
const freq = (simSource as any).frequency ?? 0
const period_from_freq = freq === 0 ? Infinity : 1 / freq
const period = (simSource as any).period ?? period_from_freq
const duty_cycle = (simSource as any).duty_cycle ?? 0.5
const pulse_width = period * duty_cycle
const delay = 0
const rise_time = "1n"
const fall_time = "1n"
value = `PULSE(${i_initial} ${i_pulsed} ${delay} ${rise_time} ${fall_time} ${pulse_width} ${period})`
}

if (value) {
const currentSourceCmd = new CurrentSourceCommand({
name: (simSource as any).simulation_current_source_id,
positiveNode,
negativeNode,
value,
})

const spiceComponent = new SpiceComponent(
(simSource as any).simulation_current_source_id,
currentSourceCmd,
[positiveNode, negativeNode],
)
netlist.addComponent(spiceComponent)
}
}
} else {
// DC Source
const positivePortId = (simSource as any).positive_source_port_id
const negativePortId = (simSource as any).negative_source_port_id

if (
positivePortId &&
negativePortId &&
"current" in simSource &&
(simSource as any).current !== undefined
) {
const positiveNode = nodeMap.get(positivePortId) || "0"
const negativeNode = nodeMap.get(negativePortId) || "0"

const currentSourceCmd = new CurrentSourceCommand({
name: (simSource as any).simulation_current_source_id,
positiveNode,
negativeNode,
value: `DC ${(simSource as any).current}`,
})

const spiceComponent = new SpiceComponent(
(simSource as any).simulation_current_source_id,
currentSourceCmd,
[positiveNode, negativeNode],
)
netlist.addComponent(spiceComponent)
}
}
}
}
90 changes: 90 additions & 0 deletions lib/processors/process-simulation-experiment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type {
AnyCircuitElement,
SimulationVoltageProbe,
SourceTrace,
} from "circuit-json"
import type { SpiceNetlist } from "lib/spice-classes/SpiceNetlist"
import { formatNumberForSpice } from "./helpers"

export const processSimulationExperiment = (
netlist: SpiceNetlist,
simExperiment: AnyCircuitElement | undefined,
simulationProbes: SimulationVoltageProbe[],
sourceTraces: SourceTrace[],
nodeMap: Map<string, string>,
) => {
if (!simExperiment) return

// Process simulation voltage probes
if (simulationProbes.length > 0) {
const nodesToProbe = new Set<string>()

const getPortIdFromNetId = (netId: string) => {
const trace = sourceTraces.find((t) =>
t.connected_source_net_ids.includes(netId),
)
return trace?.connected_source_port_ids[0]
}

for (const probe of simulationProbes) {
let signalPortId = probe.signal_input_source_port_id
if (!signalPortId) {
const signalNetId = probe.signal_input_source_net_id
if (signalNetId) {
signalPortId = getPortIdFromNetId(signalNetId)
}
}

if (!signalPortId) continue

const signalNodeName = nodeMap.get(signalPortId)
if (!signalNodeName) continue

let referencePortId = probe.reference_input_source_port_id
if (!referencePortId && probe.reference_input_source_net_id) {
referencePortId = getPortIdFromNetId(
probe.reference_input_source_net_id,
)
}

if (referencePortId) {
const referenceNodeName = nodeMap.get(referencePortId)
if (referenceNodeName && referenceNodeName !== "0") {
nodesToProbe.add(`V(${signalNodeName},${referenceNodeName})`)
} else if (signalNodeName !== "0") {
nodesToProbe.add(`V(${signalNodeName})`)
}
} else {
// Single-ended probe
if (signalNodeName !== "0") {
nodesToProbe.add(`V(${signalNodeName})`)
}
}
}

if (
nodesToProbe.size > 0 &&
(simExperiment as any).experiment_type?.includes("transient")
) {
netlist.printStatements.push(`.PRINT TRAN ${[...nodesToProbe].join(" ")}`)
}
}

const timePerStep = (simExperiment as any).time_per_step
const endTime = (simExperiment as any).end_time_ms
const startTimeMs = (simExperiment as any).start_time_ms

if (timePerStep && endTime) {
// circuit-json values are in ms, SPICE requires seconds
const startTime = (startTimeMs ?? 0) / 1000

let tranCmd = `.tran ${formatNumberForSpice(
timePerStep / 1000,
)} ${formatNumberForSpice(endTime / 1000)}`
if (startTime > 0) {
tranCmd += ` ${formatNumberForSpice(startTime)}`
}
tranCmd += " UIC"
netlist.tranCommand = tranCmd
}
}
107 changes: 107 additions & 0 deletions lib/processors/process-simulation-voltage-sources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { AnyCircuitElement } from "circuit-json"
import { SpiceComponent } from "lib/spice-classes/SpiceComponent"
import type { SpiceNetlist } from "lib/spice-classes/SpiceNetlist"
import { VoltageSourceCommand } from "lib/spice-commands"

export const processSimulationVoltageSources = (
netlist: SpiceNetlist,
simulationVoltageSources: AnyCircuitElement[],
nodeMap: Map<string, string>,
) => {
for (const simSource of simulationVoltageSources) {
if (simSource.type !== "simulation_voltage_source") continue

if ((simSource as any).is_dc_source === false) {
// AC Source
if (
"terminal1_source_port_id" in simSource &&
"terminal2_source_port_id" in simSource &&
(simSource as any).terminal1_source_port_id &&
(simSource as any).terminal2_source_port_id
) {
const positiveNode =
nodeMap.get((simSource as any).terminal1_source_port_id) || "0"
const negativeNode =
nodeMap.get((simSource as any).terminal2_source_port_id) || "0"

let value = ""
const wave_shape = (simSource as any).wave_shape
if (wave_shape === "sinewave") {
const v_offset = 0 // not provided in circuitJson
const v_peak = (simSource as any).voltage ?? 0
const freq = (simSource as any).frequency ?? 0
const delay = 0 // not provided in circuitJson
const damping_factor = 0 // not provided in circuitJson
const phase = (simSource as any).phase ?? 0
if (freq > 0) {
value = `SIN(${v_offset} ${v_peak} ${freq} ${delay} ${damping_factor} ${phase})`
} else {
value = `DC ${(simSource as any).voltage ?? 0}`
}
} else if (wave_shape === "square") {
const v_initial = 0
const v_pulsed = (simSource as any).voltage ?? 0
const freq = (simSource as any).frequency ?? 0
const period_from_freq = freq === 0 ? Infinity : 1 / freq
const period = (simSource as any).period ?? period_from_freq
const duty_cycle = (simSource as any).duty_cycle ?? 0.5
const pulse_width = period * duty_cycle
const delay = 0
const rise_time = "1n"
const fall_time = "1n"
value = `PULSE(${v_initial} ${v_pulsed} ${delay} ${rise_time} ${fall_time} ${pulse_width} ${period})`
} else if ((simSource as any).voltage !== undefined) {
value = `DC ${(simSource as any).voltage}`
}

if (value) {
const voltageSourceCmd = new VoltageSourceCommand({
name: simSource.simulation_voltage_source_id,
positiveNode,
negativeNode,
value,
})

const spiceComponent = new SpiceComponent(
simSource.simulation_voltage_source_id,
voltageSourceCmd,
[positiveNode, negativeNode],
)
netlist.addComponent(spiceComponent)
}
}
} else {
// DC Source (is_dc_source is true or undefined)
const positivePortId =
(simSource as any).positive_source_port_id ??
(simSource as any).terminal1_source_port_id
const negativePortId =
(simSource as any).negative_source_port_id ??
(simSource as any).terminal2_source_port_id

if (
positivePortId &&
negativePortId &&
"voltage" in simSource &&
(simSource as any).voltage !== undefined
) {
const positiveNode = nodeMap.get(positivePortId) || "0"
const negativeNode = nodeMap.get(negativePortId) || "0"

const voltageSourceCmd = new VoltageSourceCommand({
name: simSource.simulation_voltage_source_id,
positiveNode,
negativeNode,
value: `DC ${(simSource as any).voltage}`,
})

const spiceComponent = new SpiceComponent(
simSource.simulation_voltage_source_id,
voltageSourceCmd,
[positiveNode, negativeNode],
)
netlist.addComponent(spiceComponent)
}
}
}
}
Loading