From aac1952ebef32faeaac4005c24d86521ef8e90a0 Mon Sep 17 00:00:00 2001 From: alto555 Date: Thu, 18 Jun 2026 10:50:44 -0700 Subject: [PATCH] ckt: dangling-passive topology reduction Add CKTtopologyReduce() (cktsetup.c) to remove degree-1 dangling capacitors/resistors -- e.g. dead-end opamp compensation caps -- that otherwise cause spurious 'Timestep too small' aborts. Node degree is counted generically over all device types; the floating node is pinned with a unit diagonal. Disable with 'set no_topo_reduce'. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/spicelib/analysis/cktsetup.c | 138 +++++++++++++++++++++++++++++ src/spicelib/devices/cap/capdefs.h | 6 ++ src/spicelib/devices/cap/capload.c | 31 ++++++- src/spicelib/devices/res/resdefs.h | 4 + src/spicelib/devices/res/resload.c | 10 +++ 5 files changed, 185 insertions(+), 4 deletions(-) diff --git a/src/spicelib/analysis/cktsetup.c b/src/spicelib/analysis/cktsetup.c index 985739c64..ccdb3f9b5 100644 --- a/src/spicelib/analysis/cktsetup.c +++ b/src/spicelib/analysis/cktsetup.c @@ -13,8 +13,14 @@ Author: 1985 Thomas L. Quarles #include "ngspice/smpdefs.h" #include "ngspice/cktdefs.h" #include "ngspice/devdefs.h" +#include "ngspice/gendefs.h" #include "ngspice/sperror.h" #include "ngspice/fteext.h" +#include "ngspice/cpextern.h" + +/* device headers needed by CKTtopologyReduce() to mark dangling passives */ +#include "../devices/cap/capdefs.h" +#include "../devices/res/resdefs.h" #ifdef XSPICE #include "ngspice/enh.h" @@ -30,6 +36,131 @@ Author: 1985 Thomas L. Quarles return(E_NOMEM);\ } +/* Topology reduction for removing dangling capacitors and resistors. + * + * A node whose only connection is a single passive terminal (a degree-1 + * "dangling" node, e.g. the dead end of an opamp compensation cap) carries no + * steady current/charge, but its row becomes ill-conditioned as the timestep + * shrinks and shows up as a spurious "Timestep too small" abort. Commercial + * fast simulators simply remove such elements from the matrix. This + * pass does the same: it finds dangling passive leaves and marks the owning + * capacitor/resistor so that, at load time, the device contributes nothing and + * its floating node is pinned with a unit diagonal (kept nonsingular). + * + * Node degree is counted GENERICALLY over every device type through the + * GENnode() terminal array, so no device family can be missed (which would + * wrongly prune a live node). Only capacitors and resistors are ever removed. + * Disable with `set no_topo_reduce` in .spiceinit. */ +static void +CKTtopologyReduce(CKTcircuit *ckt) +{ + int i, t, nterm, maxnode, removed_total = 0, reported = 0; + int captype = -1, restype = -1; + int *degree; + GENmodel *gmod; + GENinstance *ginst; + + if (cp_getvar("no_topo_reduce", CP_BOOL, NULL, 0)) + return; + + maxnode = ckt->CKTmaxEqNum; + if (maxnode < 1) + return; + + degree = TMALLOC(int, maxnode + 1); + if (!degree) + return; + for (i = 0; i <= maxnode; i++) + degree[i] = 0; + + /* complete, type-agnostic node degree over all device terminals */ + for (i = 0; i < DEVmaxnum; i++) { + if (!DEVices[i] || !ckt->CKThead[i] || !DEVices[i]->DEVpublic.terms) + continue; + nterm = *(DEVices[i]->DEVpublic.terms); + if (DEVices[i]->DEVpublic.name) { + if (!strcmp(DEVices[i]->DEVpublic.name, "Capacitor")) captype = i; + else if (!strcmp(DEVices[i]->DEVpublic.name, "Resistor")) restype = i; + } + for (gmod = ckt->CKThead[i]; gmod; gmod = gmod->GENnextModel) + for (ginst = gmod->GENinstances; ginst; ginst = ginst->GENnextInstance) { + int *nodes = GENnode(ginst); + for (t = 0; t < nterm; t++) { + int nd = nodes[t]; + if (nd > 0 && nd <= maxnode) + degree[nd]++; + } + } + } + + if (captype < 0 && restype < 0) { + FREE(degree); + return; + } + + /* Leaf-prune to a fixpoint: removing a dangling passive can drop its other + * node to degree 1, exposing the next leaf of a dangling chain. */ + for (;;) { + int removed_this_pass = 0; + + if (captype >= 0) { + CAPmodel *cm; + for (cm = (CAPmodel *)ckt->CKThead[captype]; cm; cm = CAPnextModel(cm)) + for (CAPinstance *ci = CAPinstances(cm); ci; ci = CAPnextInstance(ci)) { + int pn = ci->CAPposNode, nn = ci->CAPnegNode, mode = 0; + if (ci->CAPdangling) + continue; + if (pn > 0 && degree[pn] == 1) mode |= 1; + if (nn > 0 && degree[nn] == 1) mode |= 2; + if (!mode) + continue; + ci->CAPdangling = mode; + if (mode & 1) degree[pn] = 0; else if (pn > 0) degree[pn]--; + if (mode & 2) degree[nn] = 0; else if (nn > 0) degree[nn]--; + removed_this_pass++; + if (reported++ < 40) + fprintf(stdout, + "Topology reduction: removed dangling capacitor %s " + "(floating node %s)\n", ci->CAPname, + (char *)CKTnodName(ckt, (mode & 2) ? nn : pn)); + } + } + + if (restype >= 0) { + RESmodel *rm; + for (rm = (RESmodel *)ckt->CKThead[restype]; rm; rm = RESnextModel(rm)) + for (RESinstance *ri = RESinstances(rm); ri; ri = RESnextInstance(ri)) { + int pn = ri->RESposNode, nn = ri->RESnegNode, mode = 0; + if (ri->RESdangling) + continue; + if (pn > 0 && degree[pn] == 1) mode |= 1; + if (nn > 0 && degree[nn] == 1) mode |= 2; + if (!mode) + continue; + ri->RESdangling = mode; + if (mode & 1) degree[pn] = 0; else if (pn > 0) degree[pn]--; + if (mode & 2) degree[nn] = 0; else if (nn > 0) degree[nn]--; + removed_this_pass++; + if (reported++ < 40) + fprintf(stdout, + "Topology reduction: removed dangling resistor %s " + "(floating node %s)\n", ri->RESname, + (char *)CKTnodName(ckt, (mode & 2) ? nn : pn)); + } + } + + removed_total += removed_this_pass; + if (!removed_this_pass) + break; + } + + if (removed_total) + fprintf(stdout, "Topology reduction: %d dangling passive(s) removed " + "from the matrix.\n", removed_total); + + FREE(degree); +} + int CKTsetup(CKTcircuit *ckt) { @@ -116,6 +247,13 @@ CKTsetup(CKTcircuit *ckt) } } + /* Topology reduction for removing dangling capacitors and resistors: + * mark dangling (degree-1) passive leaves + * for removal. Done after all device setups (so the node degrees are + * complete) but before the matrix is converted/bound for the linear + * solver -- it only changes load-time stamping, not the matrix structure. */ + CKTtopologyReduce(ckt); + #ifdef XSPICE /* gtri - begin - Setup for adding rshunt option resistors */ diff --git a/src/spicelib/devices/cap/capdefs.h b/src/spicelib/devices/cap/capdefs.h index 32dda86e0..3f6d10e6f 100644 --- a/src/spicelib/devices/cap/capdefs.h +++ b/src/spicelib/devices/cap/capdefs.h @@ -64,6 +64,12 @@ typedef struct sCAPinstance { unsigned CAPbv_maxGiven : 1; /* flags indicates maximum voltage is given */ int CAPsenParmNo; /* parameter # for sensitivity use; set equal to 0 if not a design parameter*/ + int CAPdangling; /* topology reduction: 0 = normal device; + bit0 set => pos node is a dangling (degree-1) floating node, + bit1 set => neg node is a dangling floating node. A dangling + cap is removed from the system: it contributes no + charge/current and its floating node(s) are pinned with a unit + diagonal so the matrix stays nonsingular. Set in CKTtopologyReduce(). */ #ifdef KLU BindElement *CAPposPosBinding ; diff --git a/src/spicelib/devices/cap/capload.c b/src/spicelib/devices/cap/capload.c index d7ed3224d..e0dcecb7a 100644 --- a/src/spicelib/devices/cap/capload.c +++ b/src/spicelib/devices/cap/capload.c @@ -50,6 +50,19 @@ CAPload(GENmodel *inModel, CKTcircuit *ckt) *(ckt->CKTrhsOld+here->CAPnegNode) ; } if(ckt->CKTmode & (MODETRAN | MODEAC)) { + if (here->CAPdangling) { + /* Topology reduction: this cap hangs on a + * floating (degree-1) node. Remove it from the system: + * pin the floating node(s) with a unit diagonal so the + * matrix stays nonsingular, and contribute no charge or + * current. This eliminates the spurious LTE pressure + * that otherwise drives "Timestep too small" at the + * dangling node (set in CKTtopologyReduce()). */ + if (here->CAPdangling & 1) *(here->CAPposPosPtr) += 1.0; + if (here->CAPdangling & 2) *(here->CAPnegNegPtr) += 1.0; + *(ckt->CKTstate0+here->CAPqcap) = 0.0; + continue; + } #ifndef PREDICTOR if(ckt->CKTmode & MODEINITPRED) { *(ckt->CKTstate0+here->CAPqcap) = @@ -71,10 +84,20 @@ CAPload(GENmodel *inModel, CKTcircuit *ckt) *(ckt->CKTstate1+here->CAPccap) = *(ckt->CKTstate0+here->CAPccap); } - *(here->CAPposPosPtr) += m * geq; - *(here->CAPnegNegPtr) += m * geq; - *(here->CAPposNegPtr) -= m * geq; - *(here->CAPnegPosPtr) -= m * geq; + /* Tiny conductance in parallel with the capacitor so that a + * node connected only through capacitors (e.g. a dangling + * compensation cap) still has a DC reference and cannot drift + * into a singular/ill-conditioned operating point. This + * restores the 1e15 ohm "avoid floating nodes" resistor that + * the old behavioral C=f(v) expansion placed across the cap; + * 1e-15 S is negligible for caps on driven nodes. */ + { + double gpar = 1e-15; + *(here->CAPposPosPtr) += m * geq + gpar; + *(here->CAPnegNegPtr) += m * geq + gpar; + *(here->CAPposNegPtr) -= m * geq + gpar; + *(here->CAPnegPosPtr) -= m * geq + gpar; + } *(ckt->CKTrhs+here->CAPposNode) -= m * ceq; *(ckt->CKTrhs+here->CAPnegNode) += m * ceq; } else diff --git a/src/spicelib/devices/res/resdefs.h b/src/spicelib/devices/res/resdefs.h index e6c2c6b39..d8a9fcc0a 100644 --- a/src/spicelib/devices/res/resdefs.h +++ b/src/spicelib/devices/res/resdefs.h @@ -81,6 +81,10 @@ typedef struct sRESinstance { unsigned RESbv_maxGiven : 1; /* flags indicates maximum voltage is given */ int RESsenParmNo; /* parameter # for sensitivity use; * set equal to 0 if not a design parameter*/ + int RESdangling; /* topology reduction: 0 = normal device; + bit0 => pos node dangling, bit1 => neg node dangling. A dangling + resistor is removed from the system and its floating node(s) + pinned with a unit diagonal. Set in CKTtopologyReduce(). */ #ifndef NONOISE double RESnVar[NSTATVARS][RESNSRCS]; diff --git a/src/spicelib/devices/res/resload.c b/src/spicelib/devices/res/resload.c index db4ac539f..884583f30 100644 --- a/src/spicelib/devices/res/resload.c +++ b/src/spicelib/devices/res/resload.c @@ -25,6 +25,16 @@ RESload(GENmodel *inModel, CKTcircuit *ckt) for (here = RESinstances(model); here != NULL ; here = RESnextInstance(here)) { + if (here->RESdangling) { + /* Topology reduction: dangling (degree-1) resistor. + * Remove it from the system, pin the floating node(s) with a + * unit diagonal (set in CKTtopologyReduce()). */ + if (here->RESdangling & 1) *(here->RESposPosPtr) += 1.0; + if (here->RESdangling & 2) *(here->RESnegNegPtr) += 1.0; + here->REScurrent = 0.0; + continue; + } + here->REScurrent = (*(ckt->CKTrhsOld+here->RESposNode) - *(ckt->CKTrhsOld+here->RESnegNode)) * here->RESconduct;