Pulsim — internal architecture¶
The Pulsim kernel is structured as seven strictly-layered C++
modules under core/include/pulsim/. Each module:
- Lives in its own subfolder (
numeric/,sparse/,topology/,models/,stamping/,pwl/,solver/,builder/,yaml/). - Depends only on layers strictly below it — compile-time enforced
through
#includediscipline. - Has its own Catch2 test binary (
pulsim_layerN_tests) that links onlypulsim::core+ Catch2. - Can be replaced wholesale without touching anything else.
This page is the entry point for understanding how the kernel is
built. End users don't need it — docs/getting-started.md and
docs/mental-model.md are the public-facing intros. Read on if you
want to extend the device library, port a new analysis, or audit
the solver.
Layered architecture¶
┌──────────────────────────────────────────────────────────────────┐
│ Layer 8: YAML loader (yaml/loader.hpp) │
├──────────────────────────────────────────────────────────────────┤
│ Layer 7: pybind11 binding (python/bindings.cpp, outside core/) │
├──────────────────────────────────────────────────────────────────┤
│ Layer 6: Builder API (builder/circuit_builder.hpp) │
├──────────────────────────────────────────────────────────────────┤
│ Layer 5: Solver (solver/run_transient.hpp + events) │
├──────────────────────────────────────────────────────────────────┤
│ Layer 4: PWL state-space (pwl/cache.hpp — the PLECS killer) │
├──────────────────────────────────────────────────────────────────┤
│ Layer 3: Stamping pipeline (stamping/stamp_device.hpp) │
├──────────────────────────────────────────────────────────────────┤
│ Layer 2: Device models (models/*.hpp, AD-driven) │
│ + motors, blockchain, analysis, switchgear, thermal │
├──────────────────────────────────────────────────────────────────┤
│ Layer 1: Topology (topology/graph.hpp + switch_state) │
├──────────────────────────────────────────────────────────────────┤
│ Layer 0: Numeric + sparse (numeric/types.hpp, sparse/*.hpp) │
└──────────────────────────────────────────────────────────────────┘
Per-layer design docs¶
The "Vopenspec/changes/archive/.
Five non-negotiable invariants¶
The kernel is built around five constraints that ripple through every layer:
- Header-only. No
.cpptranslation units in the kernel itself. The pybind11 binding (python/bindings.cpp) is the only compiled boundary. - C++23. Concepts, ranges,
mdspan-like buffers,if consteval, andstd::expected-style error returns. - AD-driven Jacobians. A single forward-mode AD scalar drives every nonlinear device's stamp; no hand-written Jacobians.
- PLECS-style PWL cache. Every reachable switch configuration is pre-factored into a (A, B, C, D) state-space tuple; the transient loop is one sparse solve per step in the linear case.
- No globals, no singletons. Every entity (Graph, DevicePool, PwlStateSpaceCache, SimulationOptions) is value-owned by the caller.
Build + test¶
# Configure
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
# Build everything (all layer test binaries + Python extension)
cmake --build build -j
# Run only Layer N's test binary
build/core/pulsim_layerN_tests
# Or run the whole ctest harness
ctest --test-dir build --output-on-failure
Each layer's test binary is independent — Layer 4 tests don't link Layer 5 sources, etc. This is the practical proof that the dependency graph really is layered.
See also¶
../mental-model.md— the end-user-facing one-page summary (Graph + DevicePool + PwlStateSpaceCache + Newton).../api-reference.md— the full Python surface in one page.openspec/changes/archive/— every OpenSpec proposal that shipped a layer or a layer milestone, with the design rationale and test coverage table.