Skip to content

Motor Models

Status: shipped. Seven motor families are wired all the way through Circuit::DeviceVariant, the YAML parser, and the Python bindings — DC, PMSM, PMSM-FOC (signal-domain controller), MechanicalDevice (shaft-only signal device for multi-shaft builds), BLDC (trapezoidal back-EMF), 3φ squirrel-cage Induction, and 1φ PSC Induction (the compressor convencional / CC motor used in legacy refrigeration compressors).

Motor models bridge the electrical and mechanical domains. Every model is pulsim::v1::DeviceVariant-aware: the Circuit reserves MNA branch rows, the stamping walker assembles the trapezoidal-companion or back-EMF terms each step, and the advance walker integrates the mechanical state forward.

Quick chooser

You want… Use YAML type: Pins
Brushed DC motor (battery + armature) DcMotorDevice dc_motor 2
Sinusoidal-back-EMF PMSM (SPM / IPM) PmsmDevice pmsm 4 (3φ + neutral)
Trapezoidal-back-EMF BLDC BldcMotorDevice bldc_motor 4
3φ squirrel-cage induction motor InductionMotorDevice induction_motor 4
1φ PSC induction motor for legacy compressors SinglePhaseInductionMotorDevice single_phase_induction_motor 2
FOC current loop (controller only, no electrical pins) PmsmFocDevice pmsm_foc 0
Standalone shaft / multi-shaft / pure-mechanical load MechanicalDevice mechanical 0

Plus the compressor / refrigerant load layer that attaches to any of the above by name — see Compressor + Refrigerant Load.

TL;DR — three end-to-end snippets

Python (BLDC drive at 6 V on phase A)

import pulsim as ps

ckt = ps.Circuit()
a = ckt.add_node("a"); b = ckt.add_node("b"); c = ckt.add_node("c")
n = ckt.add_node("n")
ckt.add_voltage_source("V_a", a, ps.Circuit.ground(), 6.0)
ckt.add_voltage_source("V_b", b, ps.Circuit.ground(), 0.0)
ckt.add_voltage_source("V_c", c, ps.Circuit.ground(), 0.0)
ckt.add_voltage_source("V_n", n, ps.Circuit.ground(), 0.0)

p = ps.BldcMotorParams()
p.R_s = 5.0; p.L_s = 8e-3; p.K_e_peak = 0.012
p.pole_pairs = 2; p.J = 5e-5; p.b_friction = 1e-5
ckt.add_bldc_motor("M1", a, b, c, n, p)

opts = ps.SimulationOptions()
opts.tstop = 5e-3
opts.dt = 1e-5
ps.Simulator(ckt, opts).run_transient()

print("ω =", ckt.bldc_omega("M1"), "rad/s")
print("τ_em =", ckt.bldc_torque("M1"), "N·m")

YAML (220 V / 60 Hz 1φ PSC compressor motor)

schema: pulsim-v1
version: 1
simulation:
  tstop: 0.2
  dt: 5e-5
components:
  - { type: sine_voltage_source, name: V_line, nodes: [line, 0],
      amplitude: 311.0, frequency: 60.0 }
  - { type: voltage_source, name: V_n, nodes: [0, 0], value: 0.0 }
  - type: single_phase_induction_motor
    name: M_cc
    nodes: [line, 0]
    R_s_main: 10.0
    L_s_main: 50e-3
    R_s_aux:  20.0
    L_s_aux:  80e-3
    C_run:    4e-6
    R_r:      8.0
    L_r:      55e-3
    L_m:      50e-3
    pole_pairs: 2
    J: 1e-4

C++ (PMSM with the FOC current loop)

#include "pulsim/v1/runtime_circuit.hpp"
using namespace pulsim::v1;

Circuit ckt;
auto a = ckt.add_node("a"); auto b = ckt.add_node("b");
auto c = ckt.add_node("c"); auto n = ckt.add_node("n");
// ... voltage sources ...

motors::PmsmParams motor_p{};
motor_p.Rs = 0.5; motor_p.Ld = motor_p.Lq = 2e-3;
motor_p.psi_pm = 0.05; motor_p.pole_pairs = 4;
motor_p.J = 1e-3; motor_p.b_friction = 1e-4;
ckt.add_pmsm("M1", a, b, c, n, motor_p);

PmsmFocDevice::Params foc_p{};
foc_p.motor = motor_p;
foc_p.foc.bandwidth_hz = 1000.0;
foc_p.foc.Vd_min = -50.0; foc_p.foc.Vd_max = 50.0;
foc_p.foc.Vq_min = -50.0; foc_p.foc.Vq_max = 50.0;
ckt.add_pmsm_foc("Ctrl1", foc_p);

Architecture: math object + device wrapper

Every motor model has two layers:

Layer Purpose Location
Math object Standalone integrator — usable in isolation for control-design unit tests core/include/pulsim/v1/motors/*.hpp
Device wrapper DeviceVariant entry — handles MNA stamping, branch-row reservation, walker integration core/include/pulsim/v1/components/*_device.hpp

The math objects (motors::Pmsm, motors::BldcMotor, motors::InductionMotor, motors::DcMotor, motors::SinglePhaseInductionMotor) compose freely in user-space C++ code without involving the Circuit at all — useful for offline tuning of FOC gains or for building custom benchmarks.

The device wrappers (PmsmDevice, BldcMotorDevice, InductionMotorDevice, DcMotorDevice, SinglePhaseInductionMotorDevice, MechanicalDevice, PmsmFocDevice) implement DynamicDeviceBase<DeviceT> — they expose stamp_impl, jacobian_pattern_impl, and update_history_impl, and the RuntimeCircuit walker drives them through the canonical DC-OP → transient → advance pipeline.

Per-model reference

DC motor (dc_motor)

Standard separately-excited armature model:

v_a  = R_a · i_a + L_a · di_a/dt + K_e · ω_m
τ_em = K_t · i_a
J · dω/dt = τ_em − τ_load − b · ω

Pins: [armature+, armature−]. The motor stamps as an inductor-in-series-with-a-back-EMF source via trapezoidal companion; the mechanical state is forward-Euler-integrated per accepted step.

Field Unit Default Note
R_a Ω 0.5 Armature resistance
L_a H 1e-3 Armature inductance
K_e V·s/rad 0.05 Back-EMF constant
K_t N·m/A 0.05 Torque constant (K_e = K_t for ideal motor)
J kg·m² 1e-4 Rotor inertia
b N·m·s 1e-5 Viscous friction
tau_load_quad_coeff N·m·s² 0 Optional τ = k·ω² load
omega_init rad/s 0 Initial speed
i_a_init A 0 Initial armature current
ckt.set_dc_motor_tau_load("M1", 0.5)   # constant 0.5 N·m brake
ω = ckt.motor_omega("M1")
i = ckt.motor_armature_current("M1")

Closed-form steady-state speed: ω_ss = (V·K_t − τ_load·R_a) / (K_t·K_e + b·R_a).

PMSM (pmsm) — sinusoidal back-EMF

Standard rotor-frame (αβ → dq) equations:

v_d = R_s·i_d + L_d·di_d/dt − ω_e · L_q · i_q
v_q = R_s·i_q + L_q·di_q/dt + ω_e · (L_d · i_d + ψ_PM)
τ_em = (3/2) · p · (ψ_PM · i_q + (L_d − L_q) · i_d · i_q)

L_d ≠ L_q produces the reluctance torque term — relevant for Interior Permanent Magnet (IPM) machines where saliency is significant. Surface PMSM (L_d = L_q) reduces to the classical τ_em = (3/2)·p·ψ_PM·i_q.

Pins: [A, B, C, N] (4-wire wye). Each phase reserves one MNA branch row stamped via per-phase trapezoidal companion + back-EMF source.

Field Unit Default
Rs Ω 0.5
Ld, Lq H 2e-3
psi_pm Wb 0.05
pole_pairs 4
J, b_friction kg·m², N·m·s 1e-3, 1e-4
omega_init, theta_init rad/s, rad 0, 0
ckt.set_pmsm_tau_load("M1", 1.0)
ω = ckt.pmsm_omega("M1")
i_a = ckt.pmsm_i_a("M1")

PMSM-FOC controller (pmsm_foc) — 0-pin signal device

Cascaded PI for i_d / i_q with pole-zero cancellation tuning auto-derived from the motor's (R_s, L_d, L_q) and a target bandwidth:

Gain Formula
K_p_d ω_c · L_d
K_i_d K_p_d · R_s / L_d
K_p_q ω_c · L_q
K_i_q K_p_q · R_s / L_q

The device has zero electrical pins: it's a signal-domain block that reads the motor's (i_d, i_q) and produces (V_d_ref, V_q_ref) each step. Feed those into an inverter / PWM modulator (or directly into a PMSM in bench mode).

- type: pmsm_foc
  name: Ctrl1
  Rs: 0.5
  Ld: 2e-3
  Lq: 2e-3
  psi_pm: 0.05
  pole_pairs: 4
  J: 1e-3
  b_friction: 1e-4
  bandwidth_hz: 1000
  Vd_min: -50
  Vd_max: 50
  Vq_min: -50
  Vq_max: 50
ckt.set_pmsm_foc_iq_ref("Ctrl1", 5.0)   # 5 A torque-producing reference
ckt.set_pmsm_foc_id_ref("Ctrl1", 0.0)
Vd_ref = ckt.pmsm_foc_vd_ref("Ctrl1")
Vq_ref = ckt.pmsm_foc_vq_ref("Ctrl1")

BLDC motor (bldc_motor) — trapezoidal back-EMF

Same 3φ pin layout as PMSM but back-EMF is trapezoidal (six 60° flat-top segments per electrical revolution). Suited for 6-step commutation drives common in low-cost BLDC controllers.

e_k(θ_e) = K_e_peak · trap(θ_e − φ_k)       # k ∈ {a, b, c}, φ_k = 0°, 120°, 240°
τ_em = (e_a·i_a + e_b·i_b + e_c·i_c) / ω_m
Field Unit Default
R_s Ω 5.0
L_s H 8e-3
K_e_peak V·s/rad 0.012
pole_pairs 2
J kg·m² 5e-5
b_friction, friction_coulomb N·m·s, N·m 1e-5, 0
ckt.set_bldc_tau_load("M1", 0.1)
ω = ckt.bldc_omega("M1")
τ = ckt.bldc_torque("M1")

3φ Induction motor (induction_motor) — squirrel cage

αβ-frame stationary model with rotor flux state ψ_r = (ψ_rα, ψ_rβ):

v_α = R_s·i_α + σ·L_s·di_α/dt + (L_m/L_r) · dψ_rα/dt
v_β = R_s·i_β + σ·L_s·di_β/dt + (L_m/L_r) · dψ_rβ/dt
dψ_rα/dt = −R_r/L_r·ψ_rα + R_r·L_m/L_r·i_α − ω_e·ψ_rβ
dψ_rβ/dt = −R_r/L_r·ψ_rβ + R_r·L_m/L_r·i_β + ω_e·ψ_rα
τ_em = (3/2) · p · (L_m/L_r) · (ψ_rα·i_β − ψ_rβ·i_α)

σ = 1 − L_m² / (L_s · L_r) is the leakage coefficient.

Pins: [A, B, C, N]. Three MNA branch rows (per-phase trapezoidal companion). At DC OP, rotor flux is zero → no back-EMF, so each phase reduces to R_s.

Field Unit Default
R_s Ω 1.0
R_r Ω 1.5
L_s, L_r H 0.15
L_m H 0.14
pole_pairs 2
J, b_friction kg·m², N·m·s 0.01, 1e-3
ckt.set_induction_tau_load("M1", 2.0)
slip = ckt.induction_slip("M1", 2*math.pi*50)  # 50 Hz synchronous
ω = ckt.induction_omega("M1")

Single-phase Induction motor (single_phase_induction_motor) — NEW

The compressor convencional (CC) motor used in pre-inverter domestic refrigerators and freezers. Permanent Split Capacitor (PSC) topology: main winding on α-axis, auxiliary winding on β-axis (90° spatial), with a permanent run capacitor in series with the auxiliary winding. The phase shift created by the cap turns a single-phase line voltage into a quasi-2φ rotating field, enabling self-start without an external inverter.

Pins: 2[line, neutral]. The run capacitor and aux winding are internal to the device. Two MNA branch rows (i_main, i_aux).

v_main = R_s_main·i_main + L_s_main·di_main/dt + (L_m/L_r)·dψ_rα/dt
v_aux − V_cap = R_s_aux·i_aux + L_s_aux·di_aux/dt + (L_m/L_r)·dψ_rβ/dt
dV_cap/dt = i_aux / C_run
dψ_rα/dt = −R_r/L_r·ψ_rα + R_r·L_m/L_r·i_main − ω_e·ψ_rβ
dψ_rβ/dt = −R_r/L_r·ψ_rβ + R_r·L_m/L_r·i_aux  + ω_e·ψ_rα
τ_em = p · (L_m/L_r) · (ψ_rα·i_aux − ψ_rβ·i_main)
Field Unit Default Note
R_s_main Ω 10.0 Main winding resistance
L_s_main H 50e-3 Main winding self-inductance
R_s_aux Ω 20.0 Auxiliary winding resistance
L_s_aux H 80e-3 Auxiliary winding self-inductance
C_run F 4e-6 Permanent run capacitor in aux branch
R_r Ω 8.0 Rotor resistance (referred to stator)
L_r H 55e-3 Rotor self-inductance
L_m H 50e-3 Mutual inductance
pole_pairs 2 4-pole = 2 pole pairs (typical fridge)
J kg·m² 1e-4 Rotor + shaft inertia
b_friction N·m·s 1e-4 Viscous friction
friction_coulomb N·m 0.05 Coulomb friction
import pulsim as ps

ckt = ps.Circuit()
line = ckt.add_node("line"); neutral = ckt.add_node("neutral")

# 220 V (RMS) / 60 Hz line.
ckt.add_sine_voltage_source("V_line", line, neutral,
                             220.0 * (2**0.5), 60.0)
ckt.add_voltage_source("V_n", neutral, ps.Circuit.ground(), 0.0)

# CC compressor motor with default Embraco-style PSC params.
p = ps.SinglePhaseInductionMotorParams()
ckt.add_single_phase_induction_motor("M_cc", line, neutral, p)

opts = ps.SimulationOptions()
opts.tstop = 0.2
opts.dt = 5e-5
ps.Simulator(ckt, opts).run_transient()

print("ω      =", ckt.single_phase_im_omega("M_cc"), "rad/s")
print("i_main =", ckt.single_phase_im_i_main("M_cc"), "A")
print("V_cap  =", ckt.single_phase_im_V_cap("M_cc"), "V")
print("τ_em   =", ckt.single_phase_im_torque("M_cc"), "N·m")

Rotation direction is a wiring choice. The PSC topology can rotate in either direction depending on the relative phase produced by the run capacitor; real CC compressors pick a direction by mechanical winding sense. Tests should compare magnitudes, not signed values.

To attach a refrigeration-cycle load:

comp = ps.compressor_defaults_for(ps.Refrigerant.R600a)
comp.displacement_m3 = 6e-6
ckt.attach_compressor_load("M_cc", comp)

See Compressor + Refrigerant Load for the full compressor docs.

MechanicalDevice (mechanical) — 0-pin shaft

Standalone shaft / inertia block with no electrical pins. Useful for multi-shaft mechanical topologies (e.g. coupling two motors through a gear, or modelling a flywheel) without forcing a motor into the build.

J · dω_m/dt = τ_input − τ_load_const − τ_load_quad·ω·|ω|
              − b_friction·ω − τ_coulomb·sign(ω)
θ_m += dt · ω_m
- type: mechanical
  name: Shaft1
  J: 1e-3
  b_friction: 1e-4
  tau_load_const: 0.5
ckt.set_mechanical_tau_input("Shaft1", 1.0)
ω = ckt.mechanical_omega("Shaft1")

Mechanical primitives (header-only)

The free-standing math helpers in motors/mechanical.hpp are still the simplest way to build custom mechanical topologies in user-space:

Helper Math Use
Shaft J·dω/dt = τ_net − b·ω − τ_C·sign(ω) Foundation block — used inside every motor
GearBox ω_out = ω_in / ratio, τ_out = τ_in · ratio · η Ideal speed reducer
ConstantTorqueLoad τ_load = τ Industrial brake / constant friction
FanLoad τ_load = k · ω · |ω| Quadratic torque profile (fans, blowers, props)
FlywheelLoad τ_load = 0, J += J_extra Pure-inertia load

Compose them in user-space when you need a mechanical layout that doesn't fit MechanicalDevice directly.

Frame transforms

motors/frame_transforms.hpp provides Clarke / Park transforms in the amplitude-invariant convention (Clarke prefactor 2/3), matching Texas Instruments / STMicro motor-control library conventions. Round-tripping abc → dq → abc at any rotor angle is identity within machine precision (pinned by test_motor_models.cpp).

auto [d, q] = motors::abc_to_dq(va, vb, vc, theta_e);
auto [Va, Vb, Vc] = motors::dq_to_abc(Vd_ref, Vq_ref, theta_e);

Validation gates

Each motor model has at least one physics gate test in the suite. The list is non-exhaustive — see core/tests/test_*motor* and test_pmsm_* for the full coverage.

Model Gate Test file
DC motor Step response → analytical ω_ss within ±5 % test_dc_motor_device.cpp
PMSM No-load: back_emf_peak = ψ_PM · p · ω_m to FP precision test_motor_models.cpp
PMSM Locked-rotor: i_q → V_q / R_s within ±1 % test_motor_models.cpp
PMSM Full transient at 100 V / 200 rad/s → analytical match test_pmsm_dynamic.cpp
PMSM-FOC Bandwidth tracking: i_q follows i_q_ref within ±5 % at design BW test_pmsm_foc_device.cpp
BLDC Trapezoidal back-EMF shape vs analytical at no load test_bldc_motor_device.cpp
BLDC Full YAML benchmark — 6-step commutation under load test_bldc_benchmark_yaml.cpp
Induction Locked-rotor inrush current envelope test_induction_motor_device.cpp
Induction No-load: rotor accelerates toward synchronous test_induction_motor_device.cpp
Induction Loaded: slip stabilises at positive value test_induction_motor_device.cpp
1φ PSC IM 60 Hz sine drive: currents bounded, rotor self-starts test_single_phase_induction_motor.cpp
1φ PSC IM Circuit-level 220 V transient: rotor reaches sync magnitude test_single_phase_induction_motor.cpp
MechanicalDevice Shaft step → ω_ss = τ/b within ±2 % at 5·τ_m test_mechanical_device.cpp

When to use which model

Application Suggested model
Bench-test a DC motor / brushed gearbox dc_motor
FOC PMSM design / current-loop tuning pmsm + pmsm_foc
BLDC controller bring-up (6-step commutation) bldc_motor
Industrial 3φ AC motor on a grid / inverter induction_motor
Legacy fixed-frequency refrigerator / freezer compressor single_phase_induction_motor
Inverter-driven modern compressor (variable-speed) bldc_motor or pmsm + pmsm_foc
Multi-shaft mechanical topology / no electrical pins mechanical
Standalone control-loop / FOC bench without electrical model pmsm_foc

See also