CKTtopologyReduce performs spurious pruning because some nodes are mistakenly taken as degree-1 dangling leaves. This happens when a node's only stamped element is a single passive and affects both ASRC and XSPICE elements. Repro netlist is attached.
--- a/src/spicelib/analysis/cktsetup.c
+++ b/src/spicelib/analysis/cktsetup.c
@@ -21,9 +21,16 @@
/* device headers needed by CKTtopologyReduce() to mark dangling passives */
#include "../devices/cap/capdefs.h"
#include "../devices/res/resdefs.h"
+/* asrc (B-source) header: its expression-sensed nodes are not GENnode()
+ * terminals, so CKTtopologyReduce() must count them explicitly (see below) */
+#include "../devices/asrc/asrcdefs.h"
#ifdef XSPICE
#include "ngspice/enh.h"
+/* XSPICE code-model header: code-model connections live in the MIF conn[]/
+ * port[] arrays (DEVpublic.terms is 0 for code models), so CKTtopologyReduce()
+ * must count their analog nodes explicitly too (see below) */
+#include "ngspice/mifdefs.h"
#endif
#ifdef USE_OMP
@@ -56,7 +63,7 @@
CKTtopologyReduce(CKTcircuit *ckt)
{
int i, t, nterm, maxnode, removed_total = 0, reported = 0;
- int captype = -1, restype = -1;
+ int captype = -1, restype = -1, asrctype = -1;
int *degree;
GENmodel *gmod;
GENinstance *ginst;
@@ -89,6 +96,8 @@
captype = i;
else if (!strcmp(DEVices[i]->DEVpublic.name, "Resistor"))
restype = i;
+ else if (!strcmp(DEVices[i]->DEVpublic.name, "ASRC"))
+ asrctype = i;
}
for (gmod = ckt->CKThead[i]; gmod; gmod = gmod->GENnextModel)
for (ginst = gmod->GENinstances; ginst; ginst = ginst->GENnextInstance) {
@@ -101,6 +110,67 @@
}
}
+ /* ASRC (B-source) controlling nodes are sensed through the parse-tree
+ * expression (ASRCvars), not through GENnode() terminals, so the generic
+ * scan above cannot see them. Count each sensed node as a connection so a
+ * node whose only stamped element is one passive is not mistaken for a
+ * dangling leaf and pruned. Pruning silently forces the sensed voltage to
+ * 0 (e.g. r.x_u1.r2 feeding a sensed op-amp input on the TI TLV9002 model).
+ * VCVS/VCCS controlling nodes are real device terminals, so they are already
+ * counted by the loop above and need no special case here. ASRCvars entries
+ * for current-controlling variables (IF_INSTANCE) are branch-equation
+ * indices, so bumping their degree is harmless. */
+ if (asrctype >= 0) {
+ ASRCmodel *am;
+ for (am = (ASRCmodel *)ckt->CKThead[asrctype]; am; am = ASRCnextModel(am))
+ for (ASRCinstance *ai = ASRCinstances(am); ai; ai = ASRCnextInstance(ai)) {
+ if (!ai->ASRCtree || !ai->ASRCvars)
+ continue;
+ for (t = 0; t < ai->ASRCtree->numVars; t++) {
+ int nd = ai->ASRCvars[t];
+ if (nd > 0 && nd <= maxnode)
+ degree[nd]++;
+ }
+ }
+ }
+
+#ifdef XSPICE
+ /* XSPICE code-model connections live in the MIF conn[]/port[] arrays; not in
+ * GENnode() terminals, and DEVpublic.terms is 0 for code models, so the
+ * generic scan above counts none of them. A node wired only to a code-model
+ * analog port plus one passive would look like a degree-1 dangling leaf and
+ * would be wrongly pruned. Count every analog port node. Code models are
+ * the device types with DEVpublic.num_conn > 0. Digital/event ports never
+ * set smp_data.pos_node/neg_node (calloc-zeroed), so the range test skips
+ * them. */
+ for (i = 0; i < DEVmaxnum; i++) {
+ if (!DEVices[i] || !ckt->CKThead[i] || DEVices[i]->DEVpublic.num_conn <= 0)
+ continue;
+ for (gmod = ckt->CKThead[i]; gmod; gmod = gmod->GENnextModel)
+ for (ginst = gmod->GENinstances; ginst; ginst = ginst->GENnextInstance) {
+ MIFinstance *mif = (MIFinstance *)ginst;
+ int c, p;
+ for (c = 0; c < mif->num_conn; c++) {
+ Mif_Conn_Data_t *cd = mif->conn[c];
+ if (!cd || cd->is_null)
+ continue;
+ for (p = 0; p < cd->size; p++) {
+ Mif_Port_Data_t *pd = cd->port[p];
+ int nd;
+ if (!pd || pd->is_null)
+ continue;
+ nd = pd->smp_data.pos_node;
+ if (nd > 0 && nd <= maxnode)
+ degree[nd]++;
+ nd = pd->smp_data.neg_node;
+ if (nd > 0 && nd <= maxnode)
+ degree[nd]++;
+ }
+ }
+ }
+ }
+#endif
+
if (captype < 0 && restype < 0) {
FREE(degree);
return;