Migration Guide — pre-1.0 → 1.0¶
Pulsim 1.0.0 shipped a brand-new kernel. The pre-1.0 API surface
(Circuit, Simulator, YamlParser, Preset, codegen,
fmu, schematic, templates, robust=True, …) was
retired during the 1.0 cycle.
This guide maps every legacy idiom to its 1.0 equivalent. If you're
hitting an AttributeError: pulsim.Foo was a v1 symbol at runtime,
look up Foo below.
At a glance¶
| Aspect | Pre-1.0 (v1) |
1.0 |
|---|---|---|
| Top-level import | import pulsim as ps |
import pulsim as p (same alias works) |
| Build a circuit | ps.Circuit() + ps.Resistor() + ps.add_component(...) |
p.CircuitBuilder() + b.add_resistor(...) |
| Run a transient | ps.Simulator(ckt, opts).run_transient(x0) |
p.simulate(b, t_end=..., dt=...) |
| Load from YAML | ps.YamlParser(opts).load("foo.yaml") |
p.load_yaml_file("foo.yaml") |
| AC sweep / Bode | Simulator.run_ac_sweep(AcSweepOptions(...)) |
p.run_ac_sweep(b, …) (MNA, fast) |
| Swept-sine FRA | Simulator.run_fra(...) |
p.run_fra(b, …) (closed-loop friendly) |
| Closed-loop control | Hand-rolled callbacks / signal_evaluator.py |
p.MixedDomainBlockChain + PIController, PIDController, … |
| Plot | matplotlib by hand | p.scope(b, res, signals=[...]), p.plot_bode(...) |
| Discovery | n/a | p.catalog(), p.example("buck_open_loop") |
The 1.0 surface is flat — there's no pulsim.v2 namespace. All
device builders, analyses, and helpers live directly on the
pulsim module.
Top-level objects¶
pulsim.Circuit → pulsim.CircuitBuilder¶
# Pre-1.0
ckt = ps.Circuit()
r1 = ckt.add_node("n1") # node names were integer IDs returned by add_node
gnd = ckt.add_node("gnd")
ckt.add_component(ps.Resistor("R1", r1, gnd, 100.0))
# 1.0 — string node names; helpers per device kind
b = p.CircuitBuilder()
b.add_resistor("R1", "n1", "gnd", 100.0)
pulsim.Simulator → pulsim.simulate(...)¶
# Pre-1.0
opts = ps.SimulationOptions(t_end=1e-3, dt=1e-5)
sim = ps.Simulator(ckt, opts)
res = sim.run_transient(ckt.initial_state())
# 1.0
res = p.simulate(b, t_end=1e-3, dt=1e-5)
For full control over the cache/solver/options pipeline, the explicit form is still available:
cache = p.PwlStateSpaceCache(b.graph, b.pool)
cache.build(dt=1e-5)
opts = p.SimulationOptions(t_start=0.0, t_end=1e-3, dt=1e-5)
res = p.run_transient(cache, b.graph, b.pool, opts,
switch_fn=lambda t: p.SwitchStateMask(0))
pulsim.YamlParser → pulsim.load_yaml_file(...)¶
# Pre-1.0
parser = ps.YamlParser(ps.YamlParserOptions())
ckt, opts = parser.load("circuit.yaml")
# 1.0
loaded = p.load_yaml_file("circuit.yaml")
b = loaded.builder
res = p.simulate(b, t_end=..., dt=...)
pulsim.Preset / pulsim.AdvancedOptions → explicit SimulationOptions¶
No global presets in 1.0. Set what you need on SimulationOptions
directly. Solver kind, Newton tolerance, line-search globalization,
event-bisection thresholds — all explicit knobs.
Device models¶
| Pre-1.0 | 1.0 |
|---|---|
ps.Resistor("R", a, b, R) |
b.add_resistor("R", a, b, R) |
ps.Capacitor("C", a, b, C) |
b.add_capacitor("C", a, b, C) |
ps.Inductor("L", a, b, L) |
b.add_inductor("L", a, b, L) |
ps.VoltageSource("V", a, b, V) |
b.add_voltage_source("V", a, b, V) |
ps.CurrentSource("I", a, b, I) |
b.add_current_source("I", a, b, I) |
ps.Diode("D", a, k, ...) |
b.add_diode("D", anode, cathode, g_on=..., g_off=..., V_th=...) |
ps.MOSFET(...) |
b.add_mosfet_level1(...) (Shichman-Hodges) or b.add_mosfet_with_body_diode(...) |
ps.IGBT(...) |
b.add_igbt_level1(...) |
ps.VoltageControlledSwitch(...) |
gate via p.SwitchStateMask + switch_fn(t) |
ps.Transformer(...) |
b.add_two_winding_transformer(...) or b.add_multi_winding_transformer(...) |
ps.SineParams(...) |
b.add_sine_voltage_source(...) |
ps.PulseParams(...) |
b.add_pulse_voltage_source(...) |
ps.PWMVoltageSource(...) |
b.add_pwm_voltage_source(...) |
ps.SaturableInductor(...) |
b.add_saturable_inductor(...) |
Control surfaces¶
The hand-rolled pre-1.0 step-callback / signal_evaluator.py flows
are replaced by the MixedDomainBlockChain executor (runs in C++,
no Python interpreter cost per step):
chain = p.MixedDomainBlockChain()
pi = chain.add_block(p.PIController(kp=0.5, ki=1200.0))
chain.wire(source="vout", to=pi.input)
chain.wire(source=pi.output, to="duty")
res = p.run_transient_with_chain(
cache, b.graph, b.pool, opts,
switch_fn, chain, chain_dt=1e-5,
)
Available blocks: PIController, PIDController, Comparator,
RateLimiter, FirstOrderLowPass, Clarke/Park/inverse,
ThyristorBlock, FuseBlock, FosterThermalNetwork, ...
Analyses¶
| Pre-1.0 | 1.0 |
|---|---|
Simulator.run_ac_sweep(...) |
p.run_ac_sweep(b, freqs=..., excite_fn=..., output_idx=...) (swept-sine) or p.run_mna_sweep(b, ...) (linearised, faster) |
Simulator.run_fra(...) |
p.run_fra(...) — swept-sine FRA (closed-loop and nonlinear-friendly) |
Simulator.run_periodic_steady_state(...) |
not yet on 1.0 — file an issue if you need it |
pulsim.sweep.run(...) (Monte-Carlo / LHS / Cartesian) |
p.sweep(builder_factory, ...) and p.monte_carlo(...) |
Features that did NOT migrate¶
| Feature | Status on 1.0.0 |
|---|---|
pulsim.codegen (C99 codegen) |
not on 1.0 |
pulsim.fmu (FMI 2.0 CS export) |
not on 1.0 |
pulsim.schematic (ELK + netlistsvg auto-layout) |
not on 1.0 |
pulsim.templates.{buck,boost,buckboost,…} (converter auto-design) |
not on 1.0 |
pulsim.Preset / AdvancedOptions global presets |
replaced by explicit SimulationOptions knobs |
compressor_load + R600a/R134a refrigerants |
not on 1.0 |
| MMC arm templates | not on 1.0 (single-arm helper deferred) |
RobustnessProfile retry-loop |
not on 1.0 (use enable_nonlinear_refresh=True + DC-OP strategies) |
| Single-phase induction motor (PSC) | not on 1.0 (DC motor, BLDC, PMSM, three-phase induction are) |
If you need any of these on 1.0, pin pulsim 0.10.x or open an
issue on the tracker — the 1.0 architecture supports each of them
as a future increment, none was retired for technical reasons.
A note on the pulsim.v2 namespace¶
Versions 0.10.x exposed a parallel pulsim.v2 surface while the
new kernel was being built next to the legacy one. With the legacy
kernel retired and the new kernel becoming the default in 1.0.0,
the v2 prefix was dropped: pulsim.v2.CircuitBuilder is now
pulsim.CircuitBuilder, pulsim.v2.simulate is pulsim.simulate,
and so on. Code that used the v2 prefix during the transition
just needs:
# Before
import pulsim.v2 as p
from pulsim.v2 import CircuitBuilder, simulate
# After
import pulsim as p
from pulsim import CircuitBuilder, simulate
The C++ side mirrors this: pulsim::v2::Foo is now pulsim::Foo,
headers moved from pulsim/v2/<sub>/<file>.hpp to
pulsim/<sub>/<file>.hpp, and the CMake alias renamed from
pulsim::v2 to pulsim::core.
1.5 → 1.6 — API stability notes¶
Three call-site shapes changed between 1.5 and 1.6.x. Each one broke working GUI integrations on the bump; flagged here so future binders don't trip on the same shape.
pulsim.sweep — package → single function¶
In 1.5, pulsim.sweep was a subpackage with helpers:
# Pre-1.6 — no longer works
from pulsim.sweep import Distribution, Cartesian, metrics
dist = Distribution.normal(mean=10.0, std=1.0)
grid = Cartesian({"R": [10, 20, 50]})
kpi = metrics.rms_voltage("vout")
In 1.6, pulsim.sweep is a single function (Cartesian grid by
default) and pulsim.monte_carlo is its random-draw sibling. The
distribution / metric helper classes were retired — callers supply
their own lambdas:
# 1.6 — grid sweep (drop-in for the old Cartesian pattern)
import pulsim as p
def build_buck(R: float):
b = p.CircuitBuilder()
... # use R inside
return b
def kpi(res, params):
import numpy as np
return {"V_out": float(np.asarray(res.v("vout"))[-1])}
res = p.sweep(
build_buck,
params={"R": [10.0, 20.0, 50.0]}, # Cartesian grid
kpi_fn=kpi,
t_end=10e-3, dt=1e-6,
)
print(res.to_dataframe())
# 1.6 — Monte Carlo (drop-in for the old Distribution pattern)
res = p.monte_carlo(
build_buck,
distributions={
"R": lambda rng: rng.normal(10.0, 1.0), # ← was Distribution.normal
},
kpi_fn=kpi,
n_samples=500,
seed=42, # reproducible
t_end=10e-3, dt=1e-6,
)
Each distributions[name] is a callable that receives a
numpy.random.Generator (the default RNG seeded by seed=) and
returns a single float. Use rng.normal, rng.uniform,
rng.lognormal, etc. directly — no helper class needed.
| 1.5 helper | 1.6 replacement |
|---|---|
pulsim.sweep.Distribution.normal(μ, σ) |
lambda rng: rng.normal(μ, σ) |
pulsim.sweep.Distribution.uniform(a, b) |
lambda rng: rng.uniform(a, b) |
pulsim.sweep.Distribution.lognormal(μ, σ) |
lambda rng: rng.lognormal(μ, σ) |
pulsim.sweep.Cartesian({...}) |
params={...} kwarg on p.sweep |
pulsim.sweep.metrics.rms_voltage(name) |
KPI lambda — extract the trace, compute RMS yourself |
pulsim.sweep.metrics.peak_current(name) |
KPI lambda — np.max(np.abs(res.i(name))) |
pulsim.sweep.metrics.settling_time(...) |
KPI lambda — your own first-time-below-2 %-band logic |
The 1.6 kpi_fn returns a dict[str, float]. Combine multiple
KPIs in one pass:
def kpi(res, params):
import numpy as np
vout = np.asarray(res.v("vout"))
iL = np.asarray(res.i("L1"))
return {
"V_out_ss": float(vout[-1]),
"V_out_rms": float(np.sqrt(np.mean(vout ** 2))),
"I_L_peak": float(np.max(np.abs(iL))),
}
This is a deliberate API simplification — the 1.5 helper classes duplicated what callers can express in three lines of numpy.
add_rc_snubber — positional → keyword-only¶
# Pre-1.6 — used to accept positional args
p.add_rc_snubber(builder, R, C, from_node, to_node) # TypeError in 1.6
# 1.6 — keyword-only
p.add_rc_snubber(
builder,
R=1.0, C=470e-9,
from_node="vdc", to_node="sw",
name_prefix="Snub", # optional, defaults to "Snub"
)
The full 1.6 signature:
def add_rc_snubber(
builder, *,
R: float, C: float,
from_node: str, to_node: str,
name_prefix: str = "Snub",
) -> None
Positional calls now raise TypeError: add_rc_snubber() takes 1
positional argument but 5 were given — the *, after builder
forces every parameter to be named.
PMSM / VSI / motors — module-level functions, no *Params struct¶
In 1.5 some helpers had a Pre-1.0-style POD params struct (e.g.
PmsmParams(R_s=..., L_s=...) → circuit.add_pmsm(params)).
1.6 standardised on module-level functions that take the parameters
as direct kwargs. There's no PmsmParams / ThreePhaseVsiParams
struct — pass the parameters directly:
# 1.6
motor = p.add_pmsm(
builder,
name="M1",
phase_nodes=("ua", "ub", "uc"),
neutral_node="nn",
R_s=0.5, Ld=1.8e-3, Lq=2.2e-3, # T2.1: per-axis Ld/Lq for IPM
psi_pm=0.05, pole_pairs=4,
J=1e-3, B=1e-4,
i_d_init=0.0, i_q_init=0.0, # T2.1: per-axis ICs
theta_init=0.0,
)
Same shape for add_three_phase_vsi, add_bldc,
add_induction_motor, etc. — pass the parameters as keyword args
on the module-level function; the helper returns a small dataclass
(e.g. PMSM, BLDC) carrying the live state. There is no separate
params struct.
If a GUI converter built against the old Params shape needs to
construct kwargs from a settings dict, just splat it:
pmsm_kwargs = {
"name": "M1",
"phase_nodes": ("ua", "ub", "uc"),
"neutral_node": "nn",
"R_s": settings["Rs"],
"Ld": settings["Ld"],
"Lq": settings["Lq"],
"psi_pm": settings["psi_pm"],
"pole_pairs": settings["pp"],
"J": settings["J"],
"B": settings.get("B", 0.0),
}
motor = p.add_pmsm(builder, **pmsm_kwargs)
This is intentional — no parallel *Params data structure to keep
in sync with the kwarg list.
See also¶
examples/scripts/— 20 runnable reference scripts.docs/tutorials/— six narrative tutorials.docs/api-reference.md— the full surface in one page.docs/gotchas.md— every footgun we've hit so far.