Skip to content

Backend C++ Contributor Guide

This guide is for contributors working in the runtime kernel under core/.

1. What is performance-critical

The most critical path is transient simulation:

  • core/src/v1/simulation.cpp
  • core/src/v1/simulation_step.cpp
  • core/src/v1/transient_services.cpp

Execution cadence is dominated by the accepted-step loop in Simulator::run_transient_native_impl(...).

If you add overhead inside that loop, total runtime cost scales with step count immediately.

2. Runtime architecture in practice

Core flow:

  1. Parser builds Circuit + SimulationOptions.
  2. Simulator wires a TransientServiceRegistry.
  3. run_transient_native_impl(...) executes adaptive/fixed stepping.
  4. Services handle assembly, nonlinear solve, segment model, recovery, losses, thermal.
  5. Outputs are written to SimulationResult (states, events, channels, telemetry).

Key boundaries:

  • TransientService interfaces in core/include/pulsim/v1/transient_services.hpp
  • Runtime module contracts in core/include/pulsim/v1/runtime_modules.hpp
  • Circuit mixed-domain execution in RuntimeCircuit
  • Python ABI surface in python/bindings.cpp

2.1 Runtime module contracts (new)

The runtime now exposes deterministic module dependency resolution for core concerns (events_topology, control_mixed_domain, loss_accounting, thermal_coupling, telemetry_channels).

Rules: - add a module by declaring module_id, provides_capabilities, requires_capabilities, and lifecycle hooks; - dependency graph must resolve deterministically (no missing providers, no cycles, no duplicate module IDs); - keep module ownership explicit for channel/telemetry outputs. - check runtime resolution through result.backend_telemetry.runtime_module_count and result.backend_telemetry.runtime_module_order.

Current non-breaking extraction status: - events_topology adapter (EventTopologyModule): - threshold proximity detection + crossing refinement hooks, - event-aligned split candidate selection, - accepted-step switch transition emission, - forced-switch sampled transition emission - control_mixed_domain adapter: core/src/v1/runtime_module_adapters.hpp/.cpp - loss_accounting adapter (LossAccountingModule): core/src/v1/runtime_module_adapters.hpp/.cpp - thermal_coupling adapter (ThermalCouplingModule): core/src/v1/runtime_module_adapters.hpp/.cpp - telemetry_channels adapter (ElectrothermalTelemetryModule): core/src/v1/runtime_module_adapters.hpp/.cpp - RuntimeModuleOrchestrator composes these adapters and exposes unified hooks: - on_run_initialize - on_sample_emit - on_step_accepted - on_hold_step_accepted - on_finalize - simulation.cpp now stays policy-focused and interacts with module internals only through the orchestrator boundary. - channel names, metadata, and Python/YAML-facing behavior remain unchanged

2.2 Dependency rules and anti-patterns

Dependency rules: - depend on capabilities, never on module IDs for runtime data flow; - one capability must have exactly one provider; - modules must be acyclic and deterministic (lexical tie-break in resolver); - module state must be self-owned; shared mutation must go through typed contracts.

Anti-patterns to avoid: - reading internal state from another module implementation; - emitting channels in multiple modules for the same canonical signal; - adding hidden ordering assumptions not declared in requires_capabilities; - allocating large buffers inside per-step/per-sample hooks when size is predictable.

3. Hot-loop coding rules

Inside per-step and per-sample paths:

  • Avoid repeated dynamic allocations.
  • Avoid repeated string formatting (std::string, std::ostringstream) on success path.
  • Avoid rebuilding summaries that can be queried incrementally.
  • Pre-reserve vectors/maps whenever capacity is estimable.

Recent optimization example:

  • Thermal virtual channels T(...) are now pre-registered once and sampled by indexed temperature access.
  • Avoided calling thermal finalize() on every sample.
  • Kept full compatibility of thermal_summary and component_electrothermal at finalize time.

4. Thermal and loss invariants

When thermal is active, preserve these invariants:

  • Thermal channels use canonical name T(<component_name>).
  • len(T(component)) == len(result.time).
  • final_temperature == last(T(component)).
  • peak_temperature == max(T(component)).
  • average_temperature == mean(T(component)).

Generation conditions for T(...):

  • simulation.enable_losses = true
  • simulation.thermal.enabled = true
  • thermal enabled for the component

5. Control and signal invariants

Frontend consumers should rely on metadata, not string heuristics.

Keep virtual_channel_metadata complete:

  • thermal channels: domain="thermal", unit="degC", source_component=<name>
  • control/event/instrumentation channels: stable domain classification

Reference: docs/frontend-control-signals.md.

6. Quality gates for backend changes

Minimum local checks for runtime changes:

cmake --build build -j
PYTHONPATH=build/python pytest python/tests/test_runtime_bindings.py -q
ctest --test-dir build --output-on-failure -R "^v1 "

If you modify parser/runtime contract fields, also validate docs and Python stubs.

Module-scoped checks (recommended before full suite):

# runtime-module contracts and ordering
ctest --test-dir build --output-on-failure -R "runtime module"

# switching/loss/thermal channel path (telemetry_channels + loss/thermal adapters)
ctest --test-dir build --output-on-failure -R "v1 switching loss accumulation|v1 fixed-step resolves multiple switching events inside one macro interval|v1 backend telemetry reports topology-driven linear cache invalidation|v1 modular electrothermal fixed-step run avoids hot-path reallocations|v1 modular electrothermal channels stay summary-consistent"

# Python contract and thermal/loss consistency
PYTHONPATH=build/python pytest -q \
  python/tests/test_runtime_bindings.py \
  python/tests/test_benchmark_python_first.py \
  python/tests/test_thermal_component_theoretical.py \
  python/tests/test_docs_release_config.py

7. What reviewers will prioritize

  • Numerical correctness and deterministic behavior.
  • Regressions in fallback/recovery telemetry.
  • Contract stability for Python and frontend consumers.
  • Runtime overhead added in hot path.
  • Test coverage for new/changed behavior.

8. Doxygen standard for backend .cpp

Keep .cpp files self-documented for community contributors:

  • Add @file header to each backend .cpp.
  • Document non-trivial helper functions and all Simulator runtime entrypoints.
  • Keep parameter/return semantics explicit in comments for stateful flows.
  • Update docs whenever behavior contracts change.

Doxygen input includes core/src, so these comments appear in generated reference docs.

9. Safe optimization strategy

  1. Confirm bottleneck with telemetry or benchmark.
  2. Optimize smallest high-impact region first.
  3. Keep behavior unchanged unless explicitly intended.
  4. Add or update tests that lock the expected contract.
  5. Document new invariants or integration expectations.