Skip to content

Python API reference

This page is a one-stop reference for the pulsim Python module. Everything here is also exposed in C++ under pulsim::* — the Python surface mirrors the C++ API.

Module overview

import pulsim as p

# High-level entry point.
p.simulate(builder, t_end, dt, **kwargs) -> SimulationResult

# Builder.
p.CircuitBuilder()
  .add_voltage_source(...)
  .add_current_source(...)
  .add_pwm_voltage_source(...)
  .add_sine_voltage_source(...)
  .add_pulse_voltage_source(...)
  .add_resistor(...)
  .add_capacitor(...)
  .add_inductor(...)
  .add_transformer(...)            # linear coupled-inductor pair, no core
  .add_inductor_coupling(...)
  .add_ideal_transformer(...)      # v_s = n v_p, i_p = -n i_s
  .add_saturable_transformer(...)  # leakage + ideal + gapped-core magnetising branch
  .add_saturable_inductor(...)
  .add_gapped_core_inductor(...)
  .add_saturable_inductor_table(...)
  .add_hysteretic_core_inductor(...)   # Jiles-Atherton, solved in the Newton loop
  .add_multi_winding_transformer(...)   # N windings, no ceiling
  .add_saturable_transformer_n(...)     # N secondaries on one gapped core
  .add_diode(...)
  .add_nonlinear_diode(...)
  .add_switch(...)
  .add_mosfet(...)
  .add_mosfet_with_body_diode(...)
  .add_mosfet_level1(...)
  .add_igbt(...)
  .add_igbt_level1(...)
  .add_vcvs(...)

# Topology / device handles.
p.Graph, p.DevicePool, p.SwitchStateMask

# Cache + simulation.
p.PwlStateSpaceCache, p.SimulationOptions, p.SimulationResult,
p.CommutationEvent, p.run_transient

# Nonlinear diode params.
p.IdealDiodeParams

# YAML loader.
p.load_yaml_string, p.load_yaml_file, p.LoadedCircuit

# Source-helper factories.
p.make_pwm_switch_fn
p.make_dead_time_pwm_pair_fn
p.make_spwm_pair_fn
p.ThreePhaseLegIndices, p.make_three_phase_spwm_fn
p.make_phase_shift_full_bridge_fn
p.make_combined_switch_fn

simulate(builder, t_end, dt, **kwargs)

The one-call high-level API (proposal #3.3). Builds the PWL cache, derives a default switch_fn, constructs SimulationOptions, runs the transient.

Arg Type Default Meaning
builder CircuitBuilder required The populated builder.
t_end float required End time, seconds.
dt float required Fixed time step, seconds.
t_start float 0.0 Start time.
switch_fn Callable[[float], SwitchStateMask] all closed Switch driver.
b_extra_fn Callable[[float], list[float]] None Per-step constant residual extras.
start_from_dc_op bool False Seed initial state from DC OP.
enable_nonlinear_refresh bool or None auto-detect Force-enable Newton refresh. None = auto via pool.has_nonlinear_devices().
max_newton_iterations int 0 (engine default) Newton iteration cap.
max_event_iterations int 0 (engine default) Event-iteration cap.

Returns a SimulationResult with parallel arrays: - result.times — list[float], length floor((t_end − t_start) / dt) + 1, or that divided by store_every (rounded up) when decimation is on. - result.states — read-only numpy.ndarray of shape (num_steps, pool.state_size(graph)). A zero-copy view over the kernel's contiguous sample buffer (v2.0): indexing, slicing, iteration and np.asarray behave as before, but access is O(1) instead of rebuilding a list of per-sample arrays. Call .copy() for a writable array. - result.num_steps() — convenience accessor. - result.states_bytes — bytes held by the sample buffer.

Pass simulate(..., store_every=m) — or set it on a SolverOptions bundle, on a raw SimulationOptions, or as simulation.store_every in YAML — to record only every m-th step. The solver still integrates at dt; the recorded grid stays strictly uniform at m · dt (so FFT / harmonic analysis on the result remains valid), and memory drops by the same factor. opts.expected_sample_count() reports what a run will store.

CircuitBuilder

Top-level fluent builder. Stores a Graph (topology) + DevicePool (per-device parameters) + an internal node-name → node-id map.

Node accessors

Method Returns Notes
b.node(name) int Get-or-create. "gnd", "GND", "0" all map to Graph.ground().
b.node_id_of(name) int Throws if name doesn't exist.
b.graph Graph The underlying topology.
b.pool DevicePool The underlying parameter pool.
b.num_branches int Branch count.

Source devices

Method Signature
add_voltage_source (name, from, to, V)
add_current_source (name, from, to, I)
add_pwm_voltage_source (name, from, to, *, v_high, v_low, frequency, duty, phase=0)
add_sine_voltage_source (name, from, to, *, v_dc=0, v_amplitude, frequency, phase=0)
add_pulse_voltage_source (name, from, to, *, v_initial, v_pulsed, t_start, pulse_width, period=0, rise_time=0, fall_time=0)
add_vcvs (name, *, in_pos, in_neg, out_pos, out_neg, gain)

Passive devices

Method Signature
add_resistor (name, from, to, R_ohms)
add_capacitor (name, from, to, C_farads)
add_inductor (name, from, to, L_henries)
add_transformer (name, primary_pos, primary_neg, secondary_pos, secondary_neg, L_p, L_s, k) — a LINEAR coupled-inductor pair: the magnetising inductance is folded into L_p and M, there is no core, and nothing in it can saturate
add_inductor_coupling (name_a, name_b, k) — couple two existing linear inductors
add_ideal_transformer (name, p_from, p_to, s_from, s_to, n) — v_s = n·v_p, i_p = −n·i_s, n = N_s/N_p; transforms DC (put a magnetising inductance across the primary if the real device has one). i(name) is the secondary current
add_saturable_transformer (name, p_from, p_to, s_from, s_to, N_p, N_s, Ae, le, lg, mu_r0=2000, B_sat=0.35, L_leak_p=0, L_leak_s=0) — T-model: per-winding leakage, ideal n, and ONE magnetising branch whose λ(i) comes from the gapped core (N_p turns, area Ae [m²], path le [m], TOTAL gap lg [m], initial μ_r, B_sat ≡ μ₀M_s [T]). i(name + ".m") is the magnetising current referred to the primary. Below the knee it equals add_transformer with L_p = L_leak_p + L_m, M = n·L_m, L_s = n²·L_m + L_leak_s
add_gapped_core_inductor (name, from, to, N, Ae, le, lg, mu_r0=2000, B_sat=0.35, knots=128) — saturable inductor from core geometry; λ(i) generated by sweeping the core field through B = μ₀(H + M_s·tanh(H/H₀)), H₀ = M_s/(μ_r0−1), with the exact slope at every knot
add_saturable_inductor (name, from, to, L_0, I_sat, L_residual=0) — the analytic law L(i) = L_res + (L_0−L_res)/(1+(i/I_sat)²), λ(i) = L_res·i + (L_0−L_res)·I_sat·atan(i/I_sat)
add_hysteretic_core_inductor (name, from, to, N, Ae, le, lg, ja, substeps_min=8, M0=0) (C++; Python: pulsim.add_hysteretic_inductor(builder, name=…, from_node=…, to_node=…, params=JilesAthertonParams, N_turns=…, l_m=…, A_core=…, l_gap=0, M0=0)) — Jiles-Atherton hysteresis INSIDE the Newton loop: M integrated from the committed (H_n, M_n) to the H that Ampère with the gap fixes for the trial current, exact L = dλ/di in the Jacobian, branch direction held per step. Replaces the observer (make_hysteretic_inductor_observer, now refusing by name), whose EMF was sign-inverted and unstable above q = L_M/(dt(R+2L_0/dt)) ≈ 0.5. HystereticInductor.bh_loop(res) replays the kernel's integrator on the current trace
add_saturable_inductor_table (name, n_pos, n_neg, i_knots, lambda_knots) — measured (i, λ) pairs for i ≥ 0 from the origin, strictly increasing; odd-extended, monotone-cubic between knots, linear beyond the last
add_multi_winding_transformer (name, windings=[(from, to, L), …], k_matrix=[[…]]) — N-winding linear coupled-inductor transformer, no ceiling (the old [2, 6] was an argument check with nothing behind it). Couplings that are not realisable — an inductance matrix that is not positive definite, e.g. k12 = k13 = 1 with k23 < 1 — are refused by name
add_saturable_transformer_n (name, p_from, p_to, N_p, L_leak_p, secondaries=[(s_from, s_to, N_s, L_leak_s), …], Ae, le, lg, mu_r0=2000, B_sat=0.35) — the saturable transformer with any number of secondaries: one gapped-core magnetising branch name.m on the primary and one ideal transformer name.s<k> per secondary, n_k = N_k/N_p

Switches + diodes

Method Signature
add_switch (name, from, to, g_on, g_off)
add_diode (name, from, to, g_on, g_off, V_th=0)
add_nonlinear_diode (name, from, to, params: IdealDiodeParams)

Nonlinear actives

Method Signature
add_mosfet (name, drain, source, R_on=1e-3, R_off=1e9) (linear switch model)
add_mosfet_with_body_diode (name, drain, source, R_on=1e-3, R_off=1e9, V_F=0.7, g_on_diode=1e3, g_off_diode=1e-9)
add_mosfet_level1 (name, drain, source, gate, K, V_T, lambda_=0.02, kappa=15.0, with_body_diode=False)
add_igbt (name, collector, emitter, R_on=10e-3, R_off=1e9) (linear)
add_igbt_level1 (name, collector, emitter, gate, V_CE_sat=2.0, R_CE_sat=0.05, V_T=5.0, kappa=10.0)

DevicePool

Opaque parameter store. Most methods are not exposed to Python — you go through CircuitBuilder. Exceptions:

Method Returns Use case
has_nonlinear_devices() bool Auto-detect by simulate().

Source-helper factories

All return a Callable[[float], SwitchStateMask] ready for switch_fn=.

make_pwm_switch_fn(switch_index, frequency, duty, phase=0)

Single-bit PWM. Sets bit switch_index ON for the first duty · T of each period.

make_dead_time_pwm_pair_fn(high_switch_index, low_switch_index, frequency, duty, dead_time, phase=0)

Complementary half-bridge with anti-shoot-through dead-time.

make_spwm_pair_fn(high_switch_index, low_switch_index, carrier_frequency, modulating_frequency, modulation_index, phase=0)

Sinusoidal modulation of the duty.

make_three_phase_spwm_fn(leg_indices, carrier_frequency, modulating_frequency, modulation_index, phase=0)

6-switch inverter. leg_indices = ThreePhaseLegIndices(a_high, a_low, b_high, b_low, c_high, c_low).

make_phase_shift_full_bridge_fn(diag1_switch_index, diag2_switch_index, frequency, phase_shift)

4-switch full-bridge ZVS phase-shifted modulation.

make_combined_switch_fn(*switch_fns)

Bit-wise OR of multiple switch_fn callables. Useful when you have an SPWM scheduler + an auxiliary low-frequency relay on the same mask.

SimulationOptions / SimulationResult

opts = p.SimulationOptions(t_start=0.0, t_end=1e-3, dt=1e-5)
opts.valid()                  # bool
opts.max_newton_iterations    # int (engine default if 0)
opts.max_event_iterations     # int
opts.newton_tol_dx            # float
opts.event_tol_v              # float
opts.event_tol_i              # float

SimulationResult: - result.times — list[float] - result.states — read-only 2-D numpy.ndarray, shape (num_steps, state_size) (zero-copy view) - result.commutations — list[CommutationEvent] (event-detection details) - result.num_steps() — int

YAML loader

loaded = p.load_yaml_file("examples/buck.yaml")
# or
loaded = p.load_yaml_string("""
nodes: [n0]
components: ...
""")
loaded.builder              # CircuitBuilder
loaded.switch_fn            # combined Callable
loaded.t_start, loaded.t_end, loaded.dt

Pass the result straight to simulate(loaded.builder, loaded.t_end, loaded.dt, switch_fn=loaded.switch_fn).

Parts library — pulsim.lib

q = p.lib.igbt("AK40N120", paths=["./parts"])   # or PULSIM_PARTS_PATH
spec  = q.switch_spec(Tj=125.0)                   # E_on_table / E_off_table + Tj
cond  = q.conduction_spec(Tj=125.0)               # V_CE_sat / R_CE_sat fitted from the on-state table
chain = q.thermal().foster_stages()               # for add_foster_network
part  = p.lib.import_plecs_xml("~/Downloads/IKW40N120T2.xml")
p.lib.save_part(part, "parts/IKW40N120T2.yaml")
  • Every part file carries a provenance: block (source, source_ref, retrieved, method ∈ transcribed | imported_plecs_xml | measured | vendor_model | synthetic); a file without one is refused by name.
  • The parts shipped with Pulsim are synthetic illustrations (their switching-energy tables are straight lines through the origin). They are listed by list_parts() but part() refuses to resolve them by number unless allow_synthetic=True.
  • Search path: directories passed as paths=, then PULSIM_PARTS_PATH, then the shipped data. The working directory is not searched; a user file shadowing a shipped one is announced once.
  • A table taken at ONE junction temperature refuses switch_spec(Tj=) at another temperature unless allow_tj_mismatch=True (audit C.1's −35 % error). A diode's recovery table becomes E_rr_curve + V_R_ref at the stated V_ref and Tj.
  • import_plecs_xml honours the PLECS sign conventions: a Diode file's switching losses sit in the −V/+I quadrant, a … with Diode file's diode in the +V/−I quadrant (split out as Erec, v_on_diode, diode_spec()). Formulas, class/type mismatches, package descriptions, gate-dependent conduction (without conduction_index) and thermal elements that do not match their branch are refused by name; Variables Min/Max become Part.limits.

Resuming a run — SolverSnapshot / simulate(resume_from=...)

first  = p.simulate(b, t_end=1e-3, dt=2e-7)          # engine='pwl'
second = p.simulate(b, t_end=5e-3, dt=2e-7, resume_from=first.final_snapshot)

result.final_snapshot carries the whole state at the end of a run: the MNA vector x, the linear trapezoidal companion history, the solver-owned diode_on bits, and one named field per stateful device kind — saturable_history (7 reals per saturable, gapped-core or hysteretic-core inductor, and per saturable-transformer magnetising branch: λ, i, v, then the Jiles-Atherton H, M, branch direction and sub-step count), coss_history (3), lauritzen_history (2), igbt_tail_history (2), pmsm_history (9 per machine). Snapshots pickle.

  • engine='pwl' resumes exactly: a two-segment run reproduces the continuous one to machine precision.
  • engine='trbdf2' / 'auto' fills a snapshot and resumes from one to the run's own tolerance — the step controller's state (last accepted h, the LTE history) is not carried, so the resumed segment restarts from h_init. A TR-BDF2 snapshot resumes exactly on the fixed engine.
  • engine='dsed' has no snapshot of its own and refuses resume_from= by name.
  • An invalid snapshot, or one whose device histories do not match the circuit, is refused by name rather than resumed from zero. Builder initial conditions (i0=, c0=) are not applied when resuming: the snapshot is the state.

steady_state refuses a circuit containing any of these devices: its one-period map is affine only while every dynamic device is a linear capacitor or inductor. Settle with a transient run and use its final_snapshot instead.

SPICE import — spice_to_builder(text_or_path, *, strict=False)

.MODEL cards are applied: D(IS N RS BV IBV) → add_shockley_diode with RS as a series resistor (BV mapped to the kernel's knee at IBV), LTspice D(Ron Roff Vfwd) → add_nonlinear_diode, LEVEL-1 NMOS → add_mosfet_level1(K = KP·W_eff/(2·L_eff)·M, V_T = VTO, λ = LAMBDA) with RD/RS series resistors and an explicit body diode when the card gives IS/JS, LTspice VDMOS → the same with K = KP/2, Rds, and the body diode from Is/N/Rb/BV. .SUBCKT instances are flattened with scoped names (XA.X1.R1, node xa.x1.mid); PARAMS: defaults and k=v on the X line are evaluated in the caller's scope; local .MODEL cards shadow outer ones; .GLOBAL nodes and 0/gnd are not scoped. Refused by name: a D/M without a model, an undefined model (.INC/.LIB are not followed), a non-LEVEL-1 MOSFET, PMOS/pchan, a bulk away from the source with GAMMA, IKF, PWL reverse breakdown, AKO:. Dropped parameters (TT/CJO, gate capacitances, RG, level-2/3 knobs) are listed in one warning; strict=True makes them refusals.

C++ namespace mapping

Python C++
pulsim.CircuitBuilder pulsim::builder::CircuitBuilder
pulsim.PwlStateSpaceCache pulsim::pwl::PwlStateSpaceCache
pulsim.run_transient pulsim::solver::run_transient
pulsim.make_pwm_switch_fn pulsim::sources::make_pwm_switch_fn

C++ code lives header-only under core/include/pulsim/. Includes you'll commonly need:

#include "pulsim/builder/circuit_builder.hpp"
#include "pulsim/pwl/pwl_state_space_cache.hpp"
#include "pulsim/solver/run_transient.hpp"
#include "pulsim/sources/make_pwm_switch_fn.hpp"
#include "pulsim/pwl/nonlinear_refresh_mosfet_level1.hpp"   // for combined refresh