YAML composite devices + chain: wiring¶
The YAML loader (pulsim.load_yaml_string /
pulsim.load_yaml_file) understands two layers:
circuit:— the electrical topology. Devices are stamped into theCircuitBuilderas usual; built-in kinds (R/L/C, MOSFET, IGBT, diode, sources, transformer, VCVS, …) plus composite devices (induction motor, hysteretic inductor) all parse here.chain:— the control layer. Block-chain wiring that runs each simulation step alongside the kernel: PMSM observer, IM observer, motor mechanical, hysteretic-core observer, etc. The loader doesn't processchain:itself; it returns the parsed builder and you callpulsim.wire_chain_from_yaml(...)to attach the chain to a runnableCxxBlockChain.
TL;DR
loaded = pulsim.load_yaml_file("im_dol.yaml") chain = pulsim.wire_chain_from_yaml(loaded, chain_spec_yaml) step_obs = chain.make_step_observer(dt) res = pulsim.simulate(loaded.builder, t_end=..., dt=dt, step_observer=step_obs, b_extra_fn=chain.make_b_extra_fn(N_state))
Composite device types in circuit:¶
induction_motor — squirrel-cage IM¶
circuit:
devices:
- {type: voltage_source, name: Va, from: a, to: gnd, V: 0.0}
- {type: voltage_source, name: Vb, from: b, to: gnd, V: 0.0}
- {type: voltage_source, name: Vc, from: c, to: gnd, V: 0.0}
- type: induction_motor
name: IM
phase_nodes: [a, b, c]
neutral_node: n
R_s: 2.4565
L_s: 0.09475
R_r: 1.22825
L_r: 0.09475
L_m: 0.087392
pole_pairs: 2
- {type: resistor, name: R_leak, from: n, to: gnd, R: 1.0e6}
The loader expands the IM into 6 named branches behind the
scenes: IM_Rs_{a,b,c} (stator winding resistances), IM_Lsig_{a,b,c}
(leakage inductances) and IM_E_{a,b,c} (back-EMF dummy sources).
The chain block resolves them via builder.branch_id_of(...).
hysteretic_inductor — Jiles-Atherton core¶
- type: hysteretic_inductor
name: L_core
from: n1
to: gnd
Ms: 4.0e5
a: 50.0
alpha: 5e-5
c: 0.20
k: 30.0
N_turns: 100
l_m: 0.05
A_core: 1e-4
Expands into L_core_L0 (air-core inductance) + L_core_V_M
(dummy voltage source carrying the magnetisation EMF). The JA
observer in the chain reads L_core_L0's branch current and
writes +N·A·µ₀·dM/dt into the L_core_V_M source row.
The chain: section¶
wire_chain_from_yaml(loaded, chain_yaml) is a separate call
because the kernel's CxxBlockChain is a different module than
the YAML circuit loader. Pass the chain spec as a YAML string OR
a Python list of dicts:
chain = pulsim.wire_chain_from_yaml(loaded, """
- type: induction_motor
device: IM
J: 0.01
B: 0.05
T_load: 0.0
R_s: 2.4565
L_s: 0.09475
R_r: 1.22825
L_r: 0.09475
L_m: 0.087392
pole_pairs: 2
omega_channel: omega
theta_channel: theta
torque_channel: torque
- type: hysteretic_inductor
device: L_core
Ms: 4.0e5
a: 50.0
alpha: 5e-5
c: 0.20
k: 30.0
N_turns: 100
l_m: 0.05
A_core: 1e-4
M_channel: M
B_channel: B
""")
Each block must carry:
type:— registered handler kind (see table below).device:— the name of the topology device this chain block attaches to. The handler resolves the deterministic branch names the loader created.
Supported chain block types (v1.5)¶
type: |
Topology device kind | Chain output channels |
|---|---|---|
induction_motor |
induction_motor |
omega, theta, psi_alpha/beta (opt), torque (opt), slip (opt) |
hysteretic_inductor |
hysteretic_inductor |
M, B, H, vm (all optional) |
sliding_mode_observer |
(pure channel — no topology device) | theta_hat, omega_hat, optional e_alpha/beta_hat, low_speed_flag |
flux_mras_observer |
(pure channel — no topology device) | omega_hat, optional psi_adj_alpha/beta, psi_ref_alpha/beta |
The two sensorless observers (sliding_mode_observer,
flux_mras_observer) are pure channel-based — they read the
αβ voltages and currents from the chain channel bus and write
estimated angle/speed back, without needing topology-resolved
branch indices. Wire them as standalone blocks alongside the IM
or PMSM that produces the αβ inputs.
Unknown block types¶
pulsim.wire_chain_from_yaml(loaded, [
{"type": "not_a_real_block", "device": "IM"}
])
# → KeyError: chain block #0 has unknown type 'not_a_real_block';
# supported: ['flux_mras_observer', 'hysteretic_inductor',
# 'induction_motor', 'sliding_mode_observer']
Honest error — no silent skip.
Empty spec¶
chain = pulsim.wire_chain_from_yaml(loaded, [])
assert chain.size() == 0
Returns a valid empty chain (zero blocks). Useful for testing the loader path without any observers attached.
simulation: block¶
The same YAML file can carry a simulation: block with solver
options:
simulation:
t_start: 0.0
t_end: 1.0e-3
dt: 1.0e-5
enable_newton_line_search: false
enable_newton_lm: false
enable_substep_state_correction: false
max_event_iterations: 16
max_newton_iterations: 50
tol_newton_dx: 1.0e-9
tol_newton_res: 1.0e-9
# Phase 2.4 schema — adaptive RK selector (v1.5).
integrator: kernel # "kernel" | "dopri5" | "radau"
rtol: 1.0e-5 # tolerances for adaptive paths
atol: 1.0e-8
dt_init: 0.0
The loader puts the parsed values on loaded.options
(a SimulationOptions instance). You can forward them into
simulate():
loaded = pulsim.load_yaml_file("buck.yaml")
res = pulsim.simulate(
loaded.builder,
t_end=loaded.options.t_end,
dt=loaded.options.dt,
integrator=loaded.options.integrator,
rtol=loaded.options.rtol,
atol=loaded.options.atol,
dt_init=loaded.options.dt_init,
)
Today only integrator="kernel" (default) actually runs;
"dopri5" and "radau" raise a clear NotImplementedError with
a pointer to the v1.6 cache refactor. The schema is wired so YAML
files stay forward-compatible. See solvers.md for
the detailed rationale.
End-to-end example¶
# 1. Load the topology + simulation options.
loaded = pulsim.load_yaml_file("examples/im_dol.yaml")
b = loaded.builder
opts = loaded.options
# 2. Wire the chain.
chain = pulsim.wire_chain_from_yaml(loaded, """
- type: induction_motor
device: IM
J: 0.01
B: 0.05
T_load: 0.0
R_s: 2.4565
L_s: 0.09475
R_r: 1.22825
L_r: 0.09475
L_m: 0.087392
pole_pairs: 2
omega_channel: omega
""")
# 3. Pull observer + b_extra wiring out of the chain.
step_obs = chain.make_step_observer(opts.dt)
b_extra_fn = chain.make_b_extra_fn(b.pool.state_size(b.graph))
# 4. Run.
res = pulsim.simulate(
b, t_end=opts.t_end, dt=opts.dt,
switch_fn=lambda t: pulsim.SwitchStateMask(0),
step_observer=step_obs,
b_extra_fn=b_extra_fn,
)
print("ω =", chain.get_channel("omega"))
Extending — add your own block type¶
Three steps:
- Write a handler function in
python/pulsim/yaml_chain.pythat takes(chain, builder, spec)and callschain.add_*for your block. - Register it in
_BLOCK_HANDLERS:_BLOCK_HANDLERS = { "induction_motor": _add_induction_motor_block, "hysteretic_inductor": _add_hysteretic_inductor_block, "sliding_mode_observer": _add_sliding_mode_observer_block, "flux_mras_observer": _add_flux_mras_observer_block, "your_new_type": _add_your_new_block, # ← here } - Add a test in
python/tests/test_yaml_chain_*.pyexercising the newtype:value end-to-end.