Circuits

Linear MNA circuits coupled to FDTD ports, SPICE import, source waveforms, nonlinear device declarations, and ESD excitations.

This page covers circuit co-simulation in witwin.maxwell (import witwin.maxwell as mw): the Circuit graph and its devices, source waveforms, parse_spice, the CircuitData result, the nonlinear device declarations, and the ESD waveforms and excitations. Use a Circuit when a port load has internal nodes, controlled sources, switches, transient sources, or connects several EM ports.

A circuit binds existing LumpedPort or TerminalPort names (see Ports). During Simulation.fdtd(...), a GPU modified nodal analysis (MNA) solve runs in the same time step as the Yee update: each bound port contributes its Norton relation, the solve returns all node voltages and port currents, and the currents go back into the port edges before the step ends. Port voltage, current, and power conventions are those of the RF ports.

Current limits: one circuit per scene in a coupled run; a bound port cannot also take a PortExcitation or a PortSweep; the initial DC solution must draw zero current from the initially zero field (ramp active sources from zero).

import witwin.maxwell as mw
 
circuit = mw.Circuit("matching_network")
inp, mid, out = circuit.node("input"), circuit.node("middle"), circuit.node("output")
circuit.add(mw.Inductor("L1", inp, mid, 2.65e-9))
circuit.add(mw.Capacitor("C1", mid, circuit.ground, 2.12e-12))
circuit.add(mw.Inductor("L2", mid, out, 2.65e-9))
circuit.bind_port("feed", positive=inp, negative=circuit.ground)
circuit.bind_port("antenna", positive=out, negative=circuit.ground)
 
scene.add_circuit(circuit)   # scene already declares ports "feed" and "antenna"
result = mw.Simulation.fdtd(scene, frequency=3e9, run_time=mw.TimeConfig(time_steps=1000)).run()
data = result.circuit("matching_network")
print(data.node_voltage("middle"), data.branch_current("L2"))

Circuit#

class Circuit(name, *, parameters=None, config=None, metadata=None)

Declarative linear circuit graph owned by a Scene. Add it with Scene(circuits=(...)) or scene.add_circuit(circuit); circuit names are unique in a scene, ignoring case.

namestrrequired

Circuit name. Non-empty, no whitespace.

parametersdict | Nonedefault None

Named parameter values, stored in circuit.parameters.

configMNAConfig | Nonedefault None

Solver settings. None uses MNAConfig().

metadatadict | Nonedefault None

Free-form metadata, stored in circuit.metadata.

Attributes: name, ground (the node "0"), parameters, initial_conditions (node name to (value, constraint)), config, metadata. Properties: nodes, devices, and bindings, each a tuple in insertion order.

Raises TypeError if config is not an MNAConfig.

Circuit.node#

Circuit.node(name: str) -> CircuitNode

Return the node with this name, creating it on first use. Lookup ignores case. "0" returns ground. Names must not contain whitespace or :.

Circuit.add#

Circuit.add(device) -> Circuit

Add a device and return the circuit. Accepted types: Resistor, Capacitor, Inductor (with CircuitNode terminals), VoltageSource, CurrentSource, the four controlled sources, MutualInductor, TimedSwitch, Diode, PiecewiseLinearIV, PolynomialIV, and VoltageDependentCapacitor. Device names are unique, ignoring case, and every terminal must be a node of this circuit.

Raises TypeError for an unsupported device, ValueError for a duplicate name or a foreign node.

Circuit.bind_port#

Circuit.bind_port(port_name: str, *, positive, negative) -> Circuit

Connect scene port port_name between two circuit nodes. positive and negative are CircuitNode objects of this circuit or node names. Each port is bound at most once per circuit; the port must exist in the scene when the run is prepared.

Circuit.set_initial_condition#

Circuit.set_initial_condition(node, value, *, constraint=True) -> Circuit

Set the initial voltage of node (a CircuitNode or name) in volts. constraint=False marks it as a guess, as in SPICE .ic ... guess. The ground node accepts only 0.

Circuit.compile#

Circuit.compile(*, available_ports=None)

Validate the topology on the CPU and return the normalized circuit graph. Checks include at least one device, unknown count within config.dense_unknown_limit, controlled-source references, dependency cycles, voltage-source loops, a DC reference, and, when available_ports is given, that every bound port is in it.

Circuit.from_spice#

Circuit.from_spice(source, *, name=None, parameters=None, include_root=None) -> Circuit

Read a netlist file. name defaults to the file stem. parameters overrides .param values. include_root is the sandbox folder for .include. See parse_spice for the supported subset.

circuit = mw.Circuit.from_spice("matching.cir", name="matching_network",
                                parameters={"Cmatch": 0.8e-12})
circuit.bind_port("feed", positive="input", negative="0")

Circuit.to_spice#

Circuit.to_spice() -> str

Return a deterministic netlist with .param, device lines, .ic, and .end. Device names must start with their SPICE letter (R, C, L, V, I, E, G, F, H, K). Raises ValueError for a TimedSwitch or a misnamed device, TypeError for a nonlinear device.

parse_spice#

parse_spice(text, *, name='circuit', parameters=None, source_path=None,
            include_root=None) -> Circuit

Parse netlist text into a Circuit. Expressions are evaluated safely; nothing executes.

textstrrequired

Netlist text.

namestrdefault 'circuit'

Name of the returned circuit.

parametersdict | Nonedefault None

Parameter overrides. Names are unique ignoring case; values are real scalars and win over .param declarations.

source_pathPath | Nonedefault None

Path of the netlist file. Required with include_root for .include.

include_rootPath | Nonedefault None

Folder that .include paths must stay inside.

Supported statements:

StatementMaps to
R, C, LResistor, Capacitor, Inductor (two nodes, one value)
V, IVoltageSource, CurrentSource, with a DC value or PULSE(...), SIN(...), PWL(...)
E, GVoltageControlledVoltageSource, VoltageControlledCurrentSource (four nodes, one gain)
F, HCurrentControlledCurrentSource, CurrentControlledVoltageSource (two nodes, control source, gain)
KMutualInductor
X with .subcktFlattened subcircuit instance (nesting depth up to 32)
.param, .ic V(node)=value [guess], .include, .endParameters, initial conditions, includes, end

Values accept SPICE suffixes (t, g, meg, k, m, u, n, p, f, mil). Unknown devices or directives, statements after .end, unsafe include paths, and duplicate names raise ValueError. Nonlinear devices and TimedSwitch have no netlist statement.

circuit = mw.parse_spice("""
L1 input middle 2.65n
C1 middle 0 2.12p
.end
""", name="match")

CircuitNode#

@dataclass(frozen=True)
class CircuitNode(name)

Named circuit node. Create nodes with Circuit.node(...) so they belong to the circuit. The name must be non-empty without whitespace.

CircuitNode.is_ground#

CircuitNode.is_ground -> bool

True when the name is "0".

PortBinding#

@dataclass(frozen=True)
class PortBinding(port_name, positive, negative)

Record created by Circuit.bind_port: a scene port name and two distinct CircuitNode terminals.

MNAConfig#

@dataclass(frozen=True)
class MNAConfig(integration='trapezoidal', pivot_tolerance=1e-12, regularization='error',
                initialization='dc', diagnostics='basic', dense_unknown_limit=256)

Settings for the MNA solve.

integrationstrdefault 'trapezoidal'

Companion model: "trapezoidal" or "backward_euler". The runtime inserts local backward-Euler steps at source discontinuities either way.

pivot_tolerancefloatdefault 1e-12

Finite, positive pivot threshold for the factorization.

regularizationstrdefault 'error'

Only "error" is supported: a singular system raises instead of being regularized.

initializationstrdefault 'dc'

"dc" or "zero" starting state.

diagnosticsstrdefault 'basic'

"basic" or "full".

dense_unknown_limitintdefault 256

Maximum number of unknowns (non-ground nodes plus branch currents) for the dense GPU solve. Positive.

VoltageSource#

class VoltageSource(name, positive, negative, voltage=0.0, *, waveform=None)

Independent voltage source between two distinct CircuitNode terminals. It adds a branch current.

namestrrequired

Device name, no whitespace.

positiveCircuitNoderequired

Positive terminal.

negativeCircuitNoderequired

Negative terminal.

voltagefloat | torch.Tensordefault 0.0

Real scalar value in volts. Used for every time sample when waveform is None.

waveformPulseWaveform | SineWaveform | PiecewiseLinearWaveform | Nonedefault None

Time-domain waveform. When set, it supplies the time samples.

CurrentSource#

class CurrentSource(name, positive, negative, current=0.0, *, waveform=None)

Independent current source. Arguments match VoltageSource, with current in amperes.

VoltageControlledVoltageSource#

class VoltageControlledVoltageSource(name, positive, negative, control_positive,
                                     control_negative, gain)

SPICE E element: output voltage gain * (V(control_positive) - V(control_negative)). Control nodes must be distinct CircuitNode objects. gain is a real scalar.

VoltageControlledCurrentSource#

class VoltageControlledCurrentSource(name, positive, negative, control_positive,
                                     control_negative, transconductance)

SPICE G element. transconductance is a real scalar in siemens.

CurrentControlledVoltageSource#

class CurrentControlledVoltageSource(name, positive, negative, control_source, transresistance)

SPICE H element. control_source names a device that carries a branch current: an Inductor, VoltageSource, VoltageControlledVoltageSource, or CurrentControlledVoltageSource. transresistance is a real scalar in ohms. Dependency cycles raise ValueError at compile.

CurrentControlledCurrentSource#

class CurrentControlledCurrentSource(name, positive, negative, control_source, gain)

SPICE F element. control_source follows the CurrentControlledVoltageSource rule. gain is a real scalar.

MutualInductor#

class MutualInductor(name, first_inductor, second_inductor, coupling)

SPICE K element: magnetic coupling between two Inductor devices of the same circuit, given by name. The names must differ, ignoring case. coupling is a real scalar with abs(coupling) at most 1.

TimedSwitch#

class TimedSwitch(name, positive, negative, transition_times, *, initially_closed=False,
                  on_resistance=1e-06, off_resistance=1000000000000.0)

Switch that toggles between two resistances at fixed times.

transition_timesSequence[float] | torch.Tensorrequired

One-dimensional, finite, non-negative, strictly increasing times in seconds. May be empty.

initially_closedbooldefault False

State before the first transition.

on_resistancefloat | torch.Tensordefault 1e-06

Closed resistance in ohms. Positive.

off_resistancefloat | torch.Tensordefault 1000000000000.0

Open resistance in ohms. Must exceed on_resistance.

PulseWaveform#

class PulseWaveform(initial, pulsed, delay=0.0, rise=0.0, fall=0.0, width=0.0, period=0.0)

SPICE PULSE waveform. All values are real scalars stored as tensors; times are in seconds and must be non-negative. Before delay the value is initial. It then ramps to pulsed over rise, holds for width, and ramps back over fall. period greater than 0 repeats the pulse; 0 means a single pulse.

SineWaveform#

class SineWaveform(offset, amplitude, frequency, delay=0.0, damping=0.0, phase_degrees=0.0)

SPICE SIN waveform: offset before delay, then offset + amplitude * exp(-damping*tau) * sin(2*pi*frequency*tau + phase) with tau = t - delay. frequency in hertz, delay in seconds, and damping in 1/s must be non-negative. phase_degrees is in degrees.

PiecewiseLinearWaveform#

class PiecewiseLinearWaveform(times, values)

SPICE PWL waveform. times (seconds) and values are real, finite, one-dimensional, of equal length, with at least two points and strictly increasing times. The value is linearly interpolated and held constant before the first and after the last point.

CircuitData#

@dataclass(frozen=True)
class CircuitData(circuit_name, times, node_names, node_voltages, branch_names,
                  branch_currents, device_powers={}, energy_balance=None, diagnostics={})

Time-domain circuit results, returned by result.circuit(name) and result.circuits. Tensors stay on the scene device and keep the autograd graph in a differentiable run.

FieldShape and meaning
circuit_nameCircuit name
times[T] sample times in seconds, strictly increasing
node_namesNode names, matching the columns of node_voltages
node_voltages[T, node] in volts
branch_namesBranch names, matching the columns of branch_currents
branch_currents[T, branch] in amperes
device_powersDevice name to [T] power samples in watts
energy_balance[T] energy-balance samples, or None
diagnosticsSolver diagnostics

All tensors share one real floating dtype and one device.

CircuitData.node_voltage#

CircuitData.node_voltage(name: str) -> torch.Tensor

Return the [T] voltage of one node. Raises KeyError for an unknown node.

CircuitData.branch_current#

CircuitData.branch_current(name: str) -> torch.Tensor

Return the [T] current of one branch. Raises KeyError for an unknown branch.

CircuitData.to#

CircuitData.to(device) -> CircuitData

Return a copy with all tensors on device.

CircuitData.save, CircuitData.load#

CircuitData.save(path: str | Path) -> None
CircuitData.load(path: str | Path, map_location=None) -> CircuitData

Save or load a detached CPU snapshot (schema version 1). load uses torch.load(weights_only=True). diagnostics must hold only tensors and primitive values.

data.save("matching-circuit.pt")
restored = mw.CircuitData.load("matching-circuit.pt")

CircuitDevice#

class CircuitDevice(Protocol)

Runtime-checkable protocol that every circuit device satisfies: a name attribute and the properties terminals (tuple of CircuitNode), parameters (dict of parameter values), and initial_condition. Use it for isinstance checks; Circuit.add still accepts only the listed device types.

Nonlinear devices#

Experimental, not yet coupled to FDTD

Diode, PiecewiseLinearIV, PolynomialIV, and VoltageDependentCapacitor can be added to a Circuit and pass topology validation. The linear MNA and FDTD runtimes reject a circuit that contains them with NotImplementedError; FDTD coupling is a later release. BJT and MOSFET raise NotImplementedError on construction.

All nonlinear devices take a name and two distinct CircuitNode terminals; the other arguments are keyword-only. They do not have a SPICE statement.

Diode#

class Diode(name, positive, negative, *, saturation_current, ideality=1.0,
            series_resistance=0.0, junction_capacitance=0.0, temperature=300.15)

Shockley junction diode, i(v) = Is * (exp(v/(n*Vt)) - 1) with Vt = k*T/q.

saturation_currentfloat | torch.Tensorrequired

Is in amperes. Positive.

idealityfloat | torch.Tensordefault 1.0

Ideality factor n. Positive.

series_resistancefloat | torch.Tensordefault 0.0

Series resistance in ohms. Non-negative.

junction_capacitancefloat | torch.Tensordefault 0.0

Junction capacitance in farads. Non-negative.

temperaturefloat | torch.Tensordefault 300.15

Temperature in kelvin. Positive.

PiecewiseLinearIV#

class PiecewiseLinearIV(name, positive, negative, *, voltages, currents)

Behavioral device with a piecewise-linear I-V curve. voltages (volts, strictly increasing, at least two knots) and currents (amperes, same length) are real, finite, one-dimensional. Current is interpolated between knots and extrapolated with the end slopes. Negative slopes are allowed.

PolynomialIV#

class PolynomialIV(name, positive, negative, *, coefficients)

Behavioral device with i(v) = sum_k coefficients[k] * v**k. coefficients is a non-empty real one-dimensional tensor, constant term first.

VoltageDependentCapacitor#

class VoltageDependentCapacitor(name, positive, negative, *, q_coefficients)

Capacitor declared by its charge q(v) = sum_k q_coefficients[k] * v**k in coulombs, so C(v) = dq/dv. It carries no DC current.

NonlinearSolveConfig#

@dataclass(frozen=True)
class NonlinearSolveConfig(relative_tolerance=1e-07, absolute_tolerance=1e-10,
                           update_relative_tolerance=1e-09, update_absolute_tolerance=1e-12,
                           max_iterations=20, line_search='backtracking',
                           max_line_search_steps=10, failure='raise',
                           gmin_start=0.0, gmin_steps=0)

Newton solve settings for nonlinear devices. An iterate is accepted only when both the residual test (relative_tolerance, absolute_tolerance) and the update test (update_relative_tolerance, update_absolute_tolerance) pass. All four tolerances are positive.

FieldMeaning
max_iterationsNewton iteration cap, at least 1
line_search"backtracking" or "none"
max_line_search_stepsAt least 1
failure"raise", or "record_and_stop" to stop and report instead
gmin_start, gmin_stepsOptional gmin continuation ladder; non-negative

No public Circuit or Simulation argument takes this config in 0.5.1.

BJT, MOSFET#

class BJT(*args, **kwargs)
class MOSFET(*args, **kwargs)

Reserved names. Construction raises NotImplementedError.

ESD excitations#

The ESD classes reproduce IEC 61000-4-2 contact-discharge currents for stress analysis. They do not model the discharge gun geometry, arcs, or failure probability, and using them is not a certification.

ClassHow it drives the scene
ESDCurrentSourceIdeal current injected across a TerminalPort gap. Add with scene.add_source(...).
ESDVoltageSourceBuilds a Circuit with the 330 ohm and 150 pF generator network bound to a port. Add the circuit with scene.add_circuit(...).
waveform = mw.ESDWaveform.iec_61000_4_2(8000.0)
# scene already declares a TerminalPort named "feed"
scene.add_source(mw.ESDCurrentSource("gun", port="feed", waveform=waveform))
result = mw.Simulation.fdtd(scene, frequencies=[1e9],
                            run_time=mw.TimeConfig(time_steps=1600)).run()
record = result.esd_waveform("gun")
print(record.diagnostics.peak_current, record.resampled.charge_ratio)

result.esd_waveform_names() lists the ESDCurrentSource names; result.esd_waveform(name) returns an ESDPortRecord.

ESDWaveform#

ESDWaveform.iec_61000_4_2(level_voltage, *, discharge='contact',
                          standard_revision=None) -> ESDWaveform

Standard ESD current as a two-term Heidler sum (exponent 1.8). The direct constructor raises TypeError; use this factory.

level_voltagefloatrequired

Test level in volts. Positive and finite. Amplitudes scale linearly from the 4 kV reference set.

dischargestrdefault 'contact'

Only "contact" is supported.

standard_revisionstr | Nonedefault None

Parameter set. None uses "ed2-contact", the only supported value.

Attributes: level_voltage, discharge, standard_revision. The waveform support is (0.0, 20 * 37e-9) seconds.

Waveform methods#

ESDWaveform and MeasuredWaveform share these members:

waveform.current(t) -> torch.Tensor
waveform.support -> tuple[float, float]
waveform.provenance -> dict
waveform.diagnostics() -> ESDDiagnostics
waveform.resample_to_grid(dt, *, t_end=None, subsamples=32) -> ESDResampledWaveform
waveform.to_source_time(*, samples=4001) -> CustomSourceTime
waveform.to_circuit_waveform(*, t_end=None, samples=4001, scale=1.0) -> PiecewiseLinearWaveform
MemberWhat it does
current(t)Current in amperes at times t in seconds
support(start, stop) time span in seconds
provenanceStandard, revision, level, model version, units
diagnostics()Peak, 30 ns and 60 ns currents, 10-90 % rise time, charge, action integral
resample_to_grid(dt, ...)Bin-averaged samples on a dt grid that conserve charge; subsamples is at least 2
to_source_time(...)Dense CustomSourceTime table with characteristic frequency 0.35 / rise_time
to_circuit_waveform(...)PiecewiseLinearWaveform of scale * current(t); samples at least 2

MeasuredWaveform#

class MeasuredWaveform(time, values, *, units='A', bandwidth=None, provenance=None)

Measured ESD current samples. Linearly interpolated, zero outside the sampled span.

timearray-likerequired

Strictly increasing sample times in seconds, at least two.

valuesarray-likerequired

Current in amperes, same length as time.

unitsstrdefault 'A'

Only "A" is supported.

bandwidthfloat | Nonedefault None

Measurement bandwidth in hertz. Positive and finite.

provenancedict | Nonedefault None

Extra metadata merged into provenance.

ESDCurrentSource#

class ESDCurrentSource(name, *, port, waveform, direction='+')

Injects a waveform as an ideal current across the gap of a TerminalPort. It lowers to a UniformCurrentSource named "<name>::current" whose density is the current divided by the port's current-contour area.

namestrrequired

Source name.

portstrrequired

Name of a TerminalPort in the scene.

waveformESDWaveform | MeasuredWaveformrequired

Current waveform.

directionstrdefault '+'

"+" drives current toward the positive terminal; "-" reverses it.

ESDCurrentSource.injection_geometry#

ESDCurrentSource.injection_geometry(scene) -> dict

Return the injection box: axis, axis_index, center, size, footprint_area, gap, and polarization_sign. Raises ValueError when the port is missing, is not a TerminalPort, or has a zero-area contour.

ESDCurrentSource.resolve#

ESDCurrentSource.resolve(scene) -> UniformCurrentSource

Lower to the uniform current source. Scene.resolved_sources() calls this.

ESDCurrentSource.provenance#

ESDCurrentSource.provenance(scene=None) -> dict

Return source metadata; with scene, it adds the injection geometry.

ESDVoltageSource#

class ESDVoltageSource(name, *, port, waveform, discharge_resistance=330.0,
                       storage_capacitance=1.5e-10)

ESD generator driven through a source network. The generator voltage is discharge_resistance * i_esd(t), so the short-circuit current equals the standard current.

namestrrequired

Generator name.

portstrrequired

Name of the TerminalPort or LumpedPort to drive.

waveformESDWaveform | MeasuredWaveformrequired

Current waveform.

discharge_resistancefloatdefault 330.0

Discharge resistance in ohms. Positive and finite.

storage_capacitancefloatdefault 1.5e-10

Storage capacitance in farads. Positive and finite.

ESDVoltageSource.build_circuit#

ESDVoltageSource.build_circuit(*, t_end=None, samples=4001, circuit_name=None,
                               config=None) -> Circuit

Return a Circuit named circuit_name (default "esd_<name>") with nodes gen and tip: a PWL voltage source from gen to ground, the discharge resistor from gen to tip, the storage capacitor from tip to ground, and the port bound from tip to ground. metadata["esd_generator"] holds the provenance. config is an optional MNAConfig.

generator = mw.ESDVoltageSource("gun", port="feed",
                                waveform=mw.ESDWaveform.iec_61000_4_2(8000.0))
scene.add_circuit(generator.build_circuit(t_end=60e-9))

ESDVoltageSource.circuit_waveform#

ESDVoltageSource.circuit_waveform(*, t_end=None, samples=4001) -> PiecewiseLinearWaveform

Return the generator drive discharge_resistance * i_esd(t).

ESDVoltageSource.provenance#

ESDVoltageSource.provenance() -> dict

Return generator metadata, element values, and units.

ESDDiagnostics#

@dataclass(frozen=True)
class ESDDiagnostics

Scalar summary of a waveform in SI units: peak_current, current_at_30ns, current_at_60ns, rise_time_10_90, charge (integral of i dt), action_integral (integral of i**2 dt), and peak_time. as_dict() returns the same values as a dict.

ESDResampledWaveform#

@dataclass(frozen=True)
class ESDResampledWaveform

Charge-conserving resampling onto a time grid. Fields: dt, times (bin centers), currents (bin-mean current), charge, action_integral, analytic_charge, analytic_action_integral. Properties: charge_ratio, action_ratio, and aliasing_metric (abs(1 - action_ratio)).

ESDPortRecord#

@dataclass(frozen=True)
class ESDPortRecord

ESD summary returned by result.esd_waveform(name).

FieldMeaning
name, port_nameSource and port names
diagnosticsESDDiagnostics of the target waveform
resampledESDResampledWaveform on the run time grid, or None if no time step is known
provenanceSource provenance dict
measuredPortData for the port if the run recorded it, otherwise None

Properties target_times and target_currents return resampled.times and resampled.currents, or None.

For ESDCurrentSource runs, measured is normally None because the ideal injection does not use the port recorder. Add a FieldTimeMonitor across the gap to observe the gap field.