diff --git a/lib/circuitJsonToSpice.ts b/lib/circuitJsonToSpice.ts index f32825f..8a3a4cd 100644 --- a/lib/circuitJsonToSpice.ts +++ b/lib/circuitJsonToSpice.ts @@ -1,14 +1,5 @@ import { SpiceNetlist } from "./spice-classes/SpiceNetlist" import { SpiceComponent } from "./spice-classes/SpiceComponent" -import { ResistorCommand } from "./spice-commands/ResistorCommand" -import { CapacitorCommand } from "./spice-commands/CapacitorCommand" -import { VoltageSourceCommand } from "./spice-commands/VoltageSourceCommand" -import { CurrentSourceCommand } from "./spice-commands/CurrentSourceCommand" -import { BJTCommand } from "./spice-commands/BJTCommand" -import { DiodeCommand } from "./spice-commands/DiodeCommand" -import { InductorCommand } from "./spice-commands/InductorCommand" -import { MOSFETCommand } from "./spice-commands/MOSFETCommand" -import { VoltageControlledSwitchCommand } from "./spice-commands/VoltageControlledSwitchCommand" import type { AnyCircuitElement, SimulationSwitch, @@ -16,6 +7,16 @@ import type { } from "circuit-json" import { getSourcePortConnectivityMapFromCircuitJson } from "circuit-json-to-connectivity-map" import { su } from "@tscircuit/circuit-json-util" +import { processSimpleResistor } from "./processors/simple-resistor" +import { processSimpleSwitch } from "./processors/simple-switch" +import { processSimpleCapacitor } from "./processors/simple-capacitor" +import { processSimpleDiode } from "./processors/simple-diode" +import { processSimpleInductor } from "./processors/simple-inductor" +import { processSimpleMosfet } from "./processors/simple-mosfet" +import { processSimpleTransistor } from "./processors/simple-transistor" +import { processSimulationVoltageSources } from "./processors/process-simulation-voltage-sources" +import { processSimulationCurrentSources } from "./processors/process-simulation-current-sources" +import { processSimulationExperiment } from "./processors/process-simulation-experiment" export function circuitJsonToSpice( circuitJson: AnyCircuitElement[], @@ -199,262 +200,53 @@ export function circuitJsonToSpice( if ("ftype" in component) { let spiceComponent: SpiceComponent | null = null - switch (component.ftype) { + switch ((component as { ftype: string }).ftype) { case "simple_resistor": { - if ("resistance" in component && "name" in component) { - const resistorCmd = new ResistorCommand({ - name: component.name, - positiveNode: nodes[0] || "0", - negativeNode: nodes[1] || "0", - value: formatResistance(component.resistance), - }) - spiceComponent = new SpiceComponent( - component.name, - resistorCmd, - nodes, - ) - } + spiceComponent = processSimpleResistor({ component, nodes }) break } case "simple_switch": { - const sanitizedBase = sanitizeIdentifier( - component.name ?? component.source_component_id, - "SW", - ) - const positiveNode = nodes[0] || "0" - const negativeNode = nodes[1] || "0" - const controlNode = `NCTRL_${sanitizedBase}` - const modelName = `SW_${sanitizedBase}` - - const associatedSimulationSwitch = simulationSwitchMap.get( - component.source_component_id, - ) - - const controlValue = buildSimulationSwitchControlValue( - associatedSimulationSwitch, - ) - - const switchCmd = new VoltageControlledSwitchCommand({ - name: sanitizedBase, - positiveNode, - negativeNode, - positiveControl: controlNode, - negativeControl: "0", - model: modelName, - }) - - spiceComponent = new SpiceComponent(sanitizedBase, switchCmd, [ - positiveNode, - negativeNode, - controlNode, - "0", - ]) - - if (!netlist.models.has(modelName)) { - netlist.models.set( - modelName, - `.MODEL ${modelName} SW(Ron=0.1 Roff=1e9 Vt=2.5 Vh=0.1)`, - ) - } - - const controlSourceName = `CTRL_${sanitizedBase}` - const controlSourceCmd = new VoltageSourceCommand({ - name: controlSourceName, - positiveNode: controlNode, - negativeNode: "0", - value: controlValue, + spiceComponent = processSimpleSwitch({ + netlist, + component, + nodes, + simulationSwitchMap, }) - - const controlComponent = new SpiceComponent( - controlSourceName, - controlSourceCmd, - [controlNode, "0"], - ) - - netlist.addComponent(controlComponent) break } - case "simple_capacitor": { - if ("capacitance" in component && "name" in component) { - const capacitorCmd = new CapacitorCommand({ - name: component.name, - positiveNode: nodes[0] || "0", - negativeNode: nodes[1] || "0", - value: formatCapacitance(component.capacitance), - }) - spiceComponent = new SpiceComponent( - component.name, - capacitorCmd, - nodes, - ) - } + spiceComponent = processSimpleCapacitor({ component, nodes }) break } case "simple_diode": { - if ("name" in component) { - const anodePort = componentPorts.find( - (p) => - p.name?.toLowerCase() === "anode" || - p.port_hints?.includes("anode"), - ) - const cathodePort = componentPorts.find( - (p) => - p.name?.toLowerCase() === "cathode" || - p.port_hints?.includes("cathode"), - ) - const positiveNode = - nodeMap.get(anodePort?.source_port_id ?? "") || "0" - const negativeNode = - nodeMap.get(cathodePort?.source_port_id ?? "") || "0" - - const modelName = "D" - const diodeCmd = new DiodeCommand({ - name: component.name, - positiveNode, - negativeNode, - model: modelName, // generic model - }) - netlist.models.set(modelName, `.MODEL ${modelName} D`) - spiceComponent = new SpiceComponent(component.name, diodeCmd, [ - positiveNode, - negativeNode, - ]) - } + spiceComponent = processSimpleDiode({ + netlist, + component, + componentPorts, + nodeMap, + }) break } case "simple_inductor": { - if ("inductance" in component && "name" in component) { - const inductorCmd = new InductorCommand({ - name: component.name, - positiveNode: nodes[0] || "0", - negativeNode: nodes[1] || "0", - value: formatInductance(component.inductance), - }) - spiceComponent = new SpiceComponent( - component.name, - inductorCmd, - nodes, - ) - } + spiceComponent = processSimpleInductor({ component, nodes }) break } case "simple_mosfet": { - if ("name" in component) { - const drainPort = componentPorts.find( - (p) => - p.name?.toLowerCase() === "drain" || - p.port_hints?.includes("drain"), - ) - const gatePort = componentPorts.find( - (p) => - p.name?.toLowerCase() === "gate" || - p.port_hints?.includes("gate"), - ) - const sourcePort = componentPorts.find( - (p) => - p.name?.toLowerCase() === "source" || - p.port_hints?.includes("source"), - ) - - const drainNode = - nodeMap.get(drainPort?.source_port_id ?? "") || "0" - const gateNode = nodeMap.get(gatePort?.source_port_id ?? "") || "0" - const sourceNode = - nodeMap.get(sourcePort?.source_port_id ?? "") || "0" - - // For 3-pin MOSFETs, substrate is typically connected to source - const substrateNode = sourceNode - - const channel_type = (component as any).channel_type ?? "n" - const mosfet_mode = (component as any).mosfet_mode ?? "enhancement" - - const modelType = `${channel_type.toUpperCase()}MOS` - const modelName = `${modelType}_${mosfet_mode.toUpperCase()}` - - if (!netlist.models.has(modelName)) { - if (mosfet_mode === "enhancement") { - const vto = channel_type === "p" ? -1 : 1 - netlist.models.set( - modelName, - `.MODEL ${modelName} ${modelType} (VTO=${vto} KP=0.1)`, - ) - } else { - netlist.models.set( - modelName, - `.MODEL ${modelName} ${modelType} (KP=0.1)`, - ) - } - } - - const mosfetCmd = new MOSFETCommand({ - name: component.name, - drain: drainNode, - gate: gateNode, - source: sourceNode, - substrate: substrateNode, - model: modelName, - }) - - spiceComponent = new SpiceComponent(component.name, mosfetCmd, [ - drainNode, - gateNode, - sourceNode, - ]) - } + spiceComponent = processSimpleMosfet({ + netlist, + component, + componentPorts, + nodeMap, + }) break } case "simple_transistor": { - if ("name" in component) { - const collectorPort = componentPorts.find( - (p) => - p.name?.toLowerCase() === "collector" || - p.port_hints?.includes("collector"), - ) - const basePort = componentPorts.find( - (p) => - p.name?.toLowerCase() === "base" || - p.port_hints?.includes("base"), - ) - const emitterPort = componentPorts.find( - (p) => - p.name?.toLowerCase() === "emitter" || - p.port_hints?.includes("emitter"), - ) - - if (!collectorPort || !basePort || !emitterPort) { - throw new Error( - `Transistor ${component.name} is missing required ports (collector, base, emitter)`, - ) - } - - const collectorNode = - nodeMap.get(collectorPort.source_port_id) || "0" - const baseNode = nodeMap.get(basePort.source_port_id) || "0" - const emitterNode = nodeMap.get(emitterPort.source_port_id) || "0" - - const transistor_type = (component as any).transistor_type ?? "npn" - const modelName = transistor_type.toUpperCase() - if (!netlist.models.has(modelName)) { - netlist.models.set( - modelName, - `.MODEL ${modelName} ${transistor_type.toUpperCase()}`, - ) - } - - const bjtCmd = new BJTCommand({ - name: component.name, - collector: collectorNode, - base: baseNode, - emitter: emitterNode, - model: modelName, - }) - spiceComponent = new SpiceComponent(component.name, bjtCmd, [ - collectorNode, - baseNode, - emitterNode, - ]) - } + spiceComponent = processSimpleTransistor({ + netlist, + component, + componentPorts, + nodeMap, + }) break } } @@ -465,368 +257,25 @@ export function circuitJsonToSpice( } } - // Process simulation voltage sources - const simulationVoltageSources = - su(circuitJson).simulation_voltage_source.list() - - 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) - } - } - } - - // Process simulation current sources - for (const simSource of su(circuitJson).simulation_current_source.list()) { - 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.simulation_current_source_id, - positiveNode, - negativeNode, - value, - }) - - const spiceComponent = new SpiceComponent( - simSource.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.simulation_current_source_id, - positiveNode, - negativeNode, - value: `DC ${(simSource as any).current}`, - }) - - const spiceComponent = new SpiceComponent( - simSource.simulation_current_source_id, - currentSourceCmd, - [positiveNode, negativeNode], - ) - netlist.addComponent(spiceComponent) - } - } - } - - const simExperiment = circuitJson.find( - (elm) => elm.type === "simulation_experiment", + processSimulationVoltageSources( + netlist, + su(circuitJson).simulation_voltage_source.list(), + nodeMap, ) - if (simExperiment) { - // Process simulation voltage probes - if (simulationProbes.length > 0) { - const nodesToProbe = new Set() - - 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 + processSimulationCurrentSources( + netlist, + su(circuitJson).simulation_current_source.list(), + nodeMap, + ) - let tranCmd = `.tran ${formatNumberForSpice( - timePerStep / 1000, - )} ${formatNumberForSpice(endTime / 1000)}` - if (startTime > 0) { - tranCmd += ` ${formatNumberForSpice(startTime)}` - } - tranCmd += " UIC" - netlist.tranCommand = tranCmd - } - } + processSimulationExperiment( + netlist, + circuitJson.find((elm) => elm.type === "simulation_experiment"), + simulationProbes, + sourceTraces, + nodeMap, + ) return netlist } - -function formatResistance(resistance: number): string { - if (resistance >= 1e6) return `${resistance / 1e6}MEG` - if (resistance >= 1e3) return `${resistance / 1e3}K` - return resistance.toString() -} - -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() -} - -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() -} - -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 -} - -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}` -} - -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() -} diff --git a/lib/processors/helpers.ts b/lib/processors/helpers.ts new file mode 100644 index 0000000..53fe2de --- /dev/null +++ b/lib/processors/helpers.ts @@ -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() +} diff --git a/lib/processors/process-simulation-current-sources.ts b/lib/processors/process-simulation-current-sources.ts new file mode 100644 index 0000000..c0d7d60 --- /dev/null +++ b/lib/processors/process-simulation-current-sources.ts @@ -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, +) => { + 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) + } + } + } +} diff --git a/lib/processors/process-simulation-experiment.ts b/lib/processors/process-simulation-experiment.ts new file mode 100644 index 0000000..26506e3 --- /dev/null +++ b/lib/processors/process-simulation-experiment.ts @@ -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, +) => { + if (!simExperiment) return + + // Process simulation voltage probes + if (simulationProbes.length > 0) { + const nodesToProbe = new Set() + + 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 + } +} diff --git a/lib/processors/process-simulation-voltage-sources.ts b/lib/processors/process-simulation-voltage-sources.ts new file mode 100644 index 0000000..2e5d0ca --- /dev/null +++ b/lib/processors/process-simulation-voltage-sources.ts @@ -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, +) => { + 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) + } + } + } +} diff --git a/lib/processors/simple-capacitor.ts b/lib/processors/simple-capacitor.ts new file mode 100644 index 0000000..30aa51f --- /dev/null +++ b/lib/processors/simple-capacitor.ts @@ -0,0 +1,23 @@ +import { SpiceComponent } from "lib/spice-classes/SpiceComponent" +import { CapacitorCommand } from "lib/spice-commands" +import type { AnyCircuitElement } from "circuit-json" +import { formatCapacitance } from "./helpers" + +export const processSimpleCapacitor = ({ + component, + nodes, +}: { + component: AnyCircuitElement + nodes: string[] +}): SpiceComponent | null => { + if ("capacitance" in component && "name" in component) { + const capacitorCmd = new CapacitorCommand({ + name: component.name as string, + positiveNode: nodes[0] || "0", + negativeNode: nodes[1] || "0", + value: formatCapacitance(component.capacitance as number), + }) + return new SpiceComponent(component.name as string, capacitorCmd, nodes) + } + return null +} diff --git a/lib/processors/simple-diode.ts b/lib/processors/simple-diode.ts new file mode 100644 index 0000000..1b26a94 --- /dev/null +++ b/lib/processors/simple-diode.ts @@ -0,0 +1,44 @@ +import type { SpiceNetlist } from "lib/spice-classes/SpiceNetlist" +import { SpiceComponent } from "lib/spice-classes/SpiceComponent" +import { DiodeCommand } from "lib/spice-commands" +import type { AnyCircuitElement } from "circuit-json" + +export const processSimpleDiode = ({ + netlist, + component, + componentPorts, + nodeMap, +}: { + netlist: SpiceNetlist + component: AnyCircuitElement + componentPorts: AnyCircuitElement[] + nodeMap: Map +}): SpiceComponent | null => { + if ("name" in component) { + const anodePort = componentPorts.find( + (p: any) => + p.name?.toLowerCase() === "anode" || p.port_hints?.includes("anode"), + ) as any + const cathodePort = componentPorts.find( + (p: any) => + p.name?.toLowerCase() === "cathode" || + p.port_hints?.includes("cathode"), + ) as any + const positiveNode = nodeMap.get(anodePort?.source_port_id ?? "") || "0" + const negativeNode = nodeMap.get(cathodePort?.source_port_id ?? "") || "0" + + const modelName = "D" + const diodeCmd = new DiodeCommand({ + name: component.name as string, + positiveNode, + negativeNode, + model: modelName, // generic model + }) + netlist.models.set(modelName, `.MODEL ${modelName} D`) + return new SpiceComponent(component.name as string, diodeCmd, [ + positiveNode, + negativeNode, + ]) + } + return null +} diff --git a/lib/processors/simple-inductor.ts b/lib/processors/simple-inductor.ts new file mode 100644 index 0000000..a6d08f7 --- /dev/null +++ b/lib/processors/simple-inductor.ts @@ -0,0 +1,23 @@ +import { SpiceComponent } from "lib/spice-classes/SpiceComponent" +import { InductorCommand } from "lib/spice-commands" +import type { AnyCircuitElement } from "circuit-json" +import { formatInductance } from "./helpers" + +export const processSimpleInductor = ({ + component, + nodes, +}: { + component: AnyCircuitElement + nodes: string[] +}): SpiceComponent | null => { + if ("inductance" in component && "name" in component) { + const inductorCmd = new InductorCommand({ + name: component.name as string, + positiveNode: nodes[0] || "0", + negativeNode: nodes[1] || "0", + value: formatInductance(component.inductance as number), + }) + return new SpiceComponent(component.name as string, inductorCmd, nodes) + } + return null +} diff --git a/lib/processors/simple-mosfet.ts b/lib/processors/simple-mosfet.ts new file mode 100644 index 0000000..60a9583 --- /dev/null +++ b/lib/processors/simple-mosfet.ts @@ -0,0 +1,75 @@ +import type { SpiceNetlist } from "lib/spice-classes/SpiceNetlist" +import { SpiceComponent } from "lib/spice-classes/SpiceComponent" +import { MOSFETCommand } from "lib/spice-commands" +import type { AnyCircuitElement } from "circuit-json" + +export const processSimpleMosfet = ({ + netlist, + component, + componentPorts, + nodeMap, +}: { + netlist: SpiceNetlist + component: AnyCircuitElement + componentPorts: AnyCircuitElement[] + nodeMap: Map +}): SpiceComponent | null => { + if ("name" in component) { + const drainPort = componentPorts.find( + (p: any) => + p.name?.toLowerCase() === "drain" || p.port_hints?.includes("drain"), + ) as any + const gatePort = componentPorts.find( + (p: any) => + p.name?.toLowerCase() === "gate" || p.port_hints?.includes("gate"), + ) as any + const sourcePort = componentPorts.find( + (p: any) => + p.name?.toLowerCase() === "source" || p.port_hints?.includes("source"), + ) as any + + const drainNode = nodeMap.get(drainPort?.source_port_id ?? "") || "0" + const gateNode = nodeMap.get(gatePort?.source_port_id ?? "") || "0" + const sourceNode = nodeMap.get(sourcePort?.source_port_id ?? "") || "0" + + // For 3-pin MOSFETs, substrate is typically connected to source + const substrateNode = sourceNode + + const channel_type = (component as any).channel_type ?? "n" + const mosfet_mode = (component as any).mosfet_mode ?? "enhancement" + + const modelType = `${channel_type.toUpperCase()}MOS` + const modelName = `${modelType}_${mosfet_mode.toUpperCase()}` + + if (!netlist.models.has(modelName)) { + if (mosfet_mode === "enhancement") { + const vto = channel_type === "p" ? -1 : 1 + netlist.models.set( + modelName, + `.MODEL ${modelName} ${modelType} (VTO=${vto} KP=0.1)`, + ) + } else { + netlist.models.set( + modelName, + `.MODEL ${modelName} ${modelType} (KP=0.1)`, + ) + } + } + + const mosfetCmd = new MOSFETCommand({ + name: component.name as string, + drain: drainNode, + gate: gateNode, + source: sourceNode, + substrate: substrateNode, + model: modelName, + }) + + return new SpiceComponent(component.name as string, mosfetCmd, [ + drainNode, + gateNode, + sourceNode, + ]) + } + return null +} diff --git a/lib/processors/simple-resistor.ts b/lib/processors/simple-resistor.ts new file mode 100644 index 0000000..01cd6fc --- /dev/null +++ b/lib/processors/simple-resistor.ts @@ -0,0 +1,23 @@ +import { SpiceComponent } from "lib/spice-classes/SpiceComponent" +import { ResistorCommand } from "lib/spice-commands" +import type { AnyCircuitElement } from "circuit-json" +import { formatResistance } from "./helpers" + +export const processSimpleResistor = ({ + component, + nodes, +}: { + component: AnyCircuitElement + nodes: string[] +}): SpiceComponent | null => { + if ("resistance" in component && "name" in component) { + const resistorCmd = new ResistorCommand({ + name: component.name as string, + positiveNode: nodes[0] || "0", + negativeNode: nodes[1] || "0", + value: formatResistance(component.resistance as number), + }) + return new SpiceComponent(component.name as string, resistorCmd, nodes) + } + return null +} diff --git a/lib/processors/simple-switch.ts b/lib/processors/simple-switch.ts new file mode 100644 index 0000000..345d112 --- /dev/null +++ b/lib/processors/simple-switch.ts @@ -0,0 +1,79 @@ +import type { SpiceNetlist } from "lib/spice-classes/SpiceNetlist" +import { SpiceComponent } from "lib/spice-classes/SpiceComponent" +import { + VoltageControlledSwitchCommand, + VoltageSourceCommand, +} from "lib/spice-commands" +import type { AnyCircuitElement, SimulationSwitch } from "circuit-json" +import { + buildSimulationSwitchControlValue, + sanitizeIdentifier, +} from "./helpers" + +export const processSimpleSwitch = ({ + netlist, + component, + nodes, + simulationSwitchMap, +}: { + netlist: SpiceNetlist + component: AnyCircuitElement + nodes: string[] + simulationSwitchMap: Map +}): SpiceComponent | null => { + const sanitizedBase = sanitizeIdentifier( + (component as any).name ?? (component as any).source_component_id, + "SW", + ) + const positiveNode = nodes[0] || "0" + const negativeNode = nodes[1] || "0" + const controlNode = `NCTRL_${sanitizedBase}` + const modelName = `SW_${sanitizedBase}` + + const associatedSimulationSwitch = simulationSwitchMap.get( + (component as any).source_component_id, + ) + + const controlValue = buildSimulationSwitchControlValue( + associatedSimulationSwitch, + ) + + const switchCmd = new VoltageControlledSwitchCommand({ + name: sanitizedBase, + positiveNode, + negativeNode, + positiveControl: controlNode, + negativeControl: "0", + model: modelName, + }) + + if (!netlist.models.has(modelName)) { + netlist.models.set( + modelName, + `.MODEL ${modelName} SW(Ron=0.1 Roff=1e9 Vt=2.5 Vh=0.1)`, + ) + } + + const controlSourceName = `CTRL_${sanitizedBase}` + const controlSourceCmd = new VoltageSourceCommand({ + name: controlSourceName, + positiveNode: controlNode, + negativeNode: "0", + value: controlValue, + }) + + const controlComponent = new SpiceComponent( + controlSourceName, + controlSourceCmd, + [controlNode, "0"], + ) + + netlist.addComponent(controlComponent) + + return new SpiceComponent(sanitizedBase, switchCmd, [ + positiveNode, + negativeNode, + controlNode, + "0", + ]) +} diff --git a/lib/processors/simple-transistor.ts b/lib/processors/simple-transistor.ts new file mode 100644 index 0000000..f0f11c9 --- /dev/null +++ b/lib/processors/simple-transistor.ts @@ -0,0 +1,66 @@ +import type { SpiceNetlist } from "lib/spice-classes/SpiceNetlist" +import { SpiceComponent } from "lib/spice-classes/SpiceComponent" +import { BJTCommand } from "lib/spice-commands" +import type { AnyCircuitElement } from "circuit-json" + +export const processSimpleTransistor = ({ + netlist, + component, + componentPorts, + nodeMap, +}: { + netlist: SpiceNetlist + component: AnyCircuitElement + componentPorts: AnyCircuitElement[] + nodeMap: Map +}): SpiceComponent | null => { + if ("name" in component) { + const collectorPort = componentPorts.find( + (p: any) => + p.name?.toLowerCase() === "collector" || + p.port_hints?.includes("collector"), + ) as any + const basePort = componentPorts.find( + (p: any) => + p.name?.toLowerCase() === "base" || p.port_hints?.includes("base"), + ) as any + const emitterPort = componentPorts.find( + (p: any) => + p.name?.toLowerCase() === "emitter" || + p.port_hints?.includes("emitter"), + ) as any + + if (!collectorPort || !basePort || !emitterPort) { + throw new Error( + `Transistor ${(component as any).name} is missing required ports (collector, base, emitter)`, + ) + } + + const collectorNode = nodeMap.get(collectorPort.source_port_id) || "0" + const baseNode = nodeMap.get(basePort.source_port_id) || "0" + const emitterNode = nodeMap.get(emitterPort.source_port_id) || "0" + + const transistor_type = (component as any).transistor_type ?? "npn" + const modelName = transistor_type.toUpperCase() + if (!netlist.models.has(modelName)) { + netlist.models.set( + modelName, + `.MODEL ${modelName} ${transistor_type.toUpperCase()}`, + ) + } + + const bjtCmd = new BJTCommand({ + name: component.name as string, + collector: collectorNode, + base: baseNode, + emitter: emitterNode, + model: modelName, + }) + return new SpiceComponent(component.name as string, bjtCmd, [ + collectorNode, + baseNode, + emitterNode, + ]) + } + return null +}