Summary
EMT::Ph3::AvVoltageSourceInverterDQ constructed with withTrafo = false segfaults during
initialisation. On that branch the constructor allocates three virtual nodes, but four sites
dereference mVirtualNodes[3] unconditionally.
Tested at 1b063c54298a46645c04c5ca156ad8178e37f486 (main, 2026-07-21), built from source with
Python bindings, EMT Ph3 / MNA, dt = 100 us.
Reproducer
examples/Notebooks/Circuits/EMT_Slack_PiLine_VSI_with_PF_Init.ipynb runs clean as committed. Change
only the with_trafo argument:
pv = dpsimpy.emt.ph3.AvVoltageSourceInverterDQ(
"pv", "pv", dpsimpy.LogLevel.debug, with_trafo=False # was True
)
and remove the now-inapplicable set_transformer_parameters(...) call. The process dies with SIGSEGV
after MnaSolverFactory ... creating KLUAdapter solver implementation, i.e. inside
initializeFromNodesAndTerminals, before the first step. Nothing else in the notebook is touched.
Cause
dpsim-models/src/EMT/EMT_Ph3_AvVoltageSourceInverterDQ.cpp, constructor:
if (withTrafo) {
setVirtualNodeNumber(4);
...
} else {
setVirtualNodeNumber(3);
}
With the transformer, index 3 is the filter-interface node. Without it, index 3 is one past the end.
These four dereferences are not guarded by mWithConnectionTransformer:
L277 Real theta = std::arg(mVirtualNodes[3]->initialSingleVoltage());
L291 matrixStateInit(0, 0) = std::arg(mVirtualNodes[3]->initialSingleVoltage());
L292 matrixOutputInit(0, 0) = std::arg(mVirtualNodes[3]->initialSingleVoltage());
L413 vcdq = parkTransformPowerInvariant(theta, **mVirtualNodes[3]->mVoltage);
The first three are in initializeFromNodesAndTerminals (where the crash lands); L413 is in the
per-step control update, so it would fire every step if initialisation were survived.
Note the surrounding code already handles the no-transformer case correctly — e.g. L263-266 picks
the terminal node instead of mVirtualNodes[3] for mSubResistorC, and L217-236 branches properly
on the initial-voltage calculation. It appears only these four references were missed.
Suggested fix
In each of the four places, use the node that is electrically equivalent when there is no transformer,
which is the one mSubResistorC is already connected to on that branch — mTerminals[0]->node().
Something of the shape:
auto filterIfNode = mWithConnectionTransformer
? mVirtualNodes[3]
: mTerminals[0]->node();
then use filterIfNode at all four sites. That keeps the with-transformer path bit-identical.
Why it matters
For a low-voltage island there is no step-up transformer to model, so withTrafo = false is the
physically correct construction, and it is reachable from the Python bindings with no warning. A
guard, or an explicit "not supported" throw, would at least turn a segfault into something
diagnosable.
Happy to open a PR if the suggested shape looks right.
Summary
EMT::Ph3::AvVoltageSourceInverterDQconstructed withwithTrafo = falsesegfaults duringinitialisation. On that branch the constructor allocates three virtual nodes, but four sites
dereference
mVirtualNodes[3]unconditionally.Tested at
1b063c54298a46645c04c5ca156ad8178e37f486(main, 2026-07-21), built from source withPython bindings, EMT Ph3 / MNA,
dt = 100 us.Reproducer
examples/Notebooks/Circuits/EMT_Slack_PiLine_VSI_with_PF_Init.ipynbruns clean as committed. Changeonly the
with_trafoargument:and remove the now-inapplicable
set_transformer_parameters(...)call. The process dies with SIGSEGVafter
MnaSolverFactory ... creating KLUAdapter solver implementation, i.e. insideinitializeFromNodesAndTerminals, before the first step. Nothing else in the notebook is touched.Cause
dpsim-models/src/EMT/EMT_Ph3_AvVoltageSourceInverterDQ.cpp, constructor:With the transformer, index 3 is the filter-interface node. Without it, index 3 is one past the end.
These four dereferences are not guarded by
mWithConnectionTransformer:L277Real theta = std::arg(mVirtualNodes[3]->initialSingleVoltage());L291matrixStateInit(0, 0) = std::arg(mVirtualNodes[3]->initialSingleVoltage());L292matrixOutputInit(0, 0) = std::arg(mVirtualNodes[3]->initialSingleVoltage());L413vcdq = parkTransformPowerInvariant(theta, **mVirtualNodes[3]->mVoltage);The first three are in
initializeFromNodesAndTerminals(where the crash lands);L413is in theper-step control update, so it would fire every step if initialisation were survived.
Note the surrounding code already handles the no-transformer case correctly — e.g.
L263-266picksthe terminal node instead of
mVirtualNodes[3]formSubResistorC, andL217-236branches properlyon the initial-voltage calculation. It appears only these four references were missed.
Suggested fix
In each of the four places, use the node that is electrically equivalent when there is no transformer,
which is the one
mSubResistorCis already connected to on that branch —mTerminals[0]->node().Something of the shape:
then use
filterIfNodeat all four sites. That keeps the with-transformer path bit-identical.Why it matters
For a low-voltage island there is no step-up transformer to model, so
withTrafo = falseis thephysically correct construction, and it is reachable from the Python bindings with no warning. A
guard, or an explicit "not supported" throw, would at least turn a segfault into something
diagnosable.
Happy to open a PR if the suggested shape looks right.